diff --git a/cypress/e2e/27-two-factor-authentication.cy.ts b/cypress/e2e/27-two-factor-authentication.cy.ts index dc62a0c58c087..81426a0ddeb88 100644 --- a/cypress/e2e/27-two-factor-authentication.cy.ts +++ b/cypress/e2e/27-two-factor-authentication.cy.ts @@ -68,14 +68,28 @@ describe('Two-factor authentication', { disableAutoLogin: true }, () => { mainSidebar.actions.signout(); }); - it('Should be able to disable MFA in account', () => { + it('Should be able to disable MFA in account with MFA token ', () => { const { email, password } = user; signinPage.actions.loginWithEmailAndPassword(email, password); personalSettingsPage.actions.enableMfa(); mainSidebar.actions.signout(); - const token = generateOTPToken(user.mfaSecret); - mfaLoginPage.actions.loginWithMfaToken(email, password, token); - personalSettingsPage.actions.disableMfa(); + const loginToken = generateOTPToken(user.mfaSecret); + mfaLoginPage.actions.loginWithMfaToken(email, password, loginToken); + const disableToken = generateOTPToken(user.mfaSecret); + personalSettingsPage.actions.disableMfa(disableToken); + personalSettingsPage.getters.enableMfaButton().should('exist'); + mainSidebar.actions.signout(); + }); + + it('Should be able to disable MFA in account with recovery code', () => { + const { email, password } = user; + signinPage.actions.loginWithEmailAndPassword(email, password); + personalSettingsPage.actions.enableMfa(); + mainSidebar.actions.signout(); + const loginToken = generateOTPToken(user.mfaSecret); + mfaLoginPage.actions.loginWithMfaToken(email, password, loginToken); + personalSettingsPage.actions.disableMfa(user.mfaRecoveryCodes[0]); + personalSettingsPage.getters.enableMfaButton().should('exist'); mainSidebar.actions.signout(); }); }); diff --git a/cypress/pages/settings-personal.ts b/cypress/pages/settings-personal.ts index 4574f95691fa8..5602bd7e9233a 100644 --- a/cypress/pages/settings-personal.ts +++ b/cypress/pages/settings-personal.ts @@ -22,6 +22,8 @@ export class PersonalSettingsPage extends BasePage { saveSettingsButton: () => cy.getByTestId('save-settings-button'), enableMfaButton: () => cy.getByTestId('enable-mfa-button'), disableMfaButton: () => cy.getByTestId('disable-mfa-button'), + mfaCodeOrMfaRecoveryCodeInput: () => cy.getByTestId('mfa-code-or-recovery-code-input'), + mfaSaveButton: () => cy.getByTestId('mfa-save-button'), themeSelector: () => cy.getByTestId('theme-select'), selectOptionsVisible: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'), }; @@ -83,9 +85,11 @@ export class PersonalSettingsPage extends BasePage { mfaSetupModal.getters.saveButton().click(); }); }, - disableMfa: () => { + disableMfa: (mfaCodeOrRecoveryCode: string) => { cy.visit(this.url); this.getters.disableMfaButton().click(); + this.getters.mfaCodeOrMfaRecoveryCodeInput().type(mfaCodeOrRecoveryCode); + this.getters.mfaSaveButton().click(); }, }; } diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index c2ee1c92fbc67..409c06ed90517 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -41,7 +41,7 @@ export class AuthController { /** Log in a user */ @Post('/login', { skipAuth: true, rateLimit: true }) async login(req: LoginRequest, res: Response): Promise { - const { email, password, mfaToken, mfaRecoveryCode } = req.body; + const { email, password, mfaCode, mfaRecoveryCode } = req.body; if (!email) throw new ApplicationError('Email is required to log in'); if (!password) throw new ApplicationError('Password is required to log in'); @@ -75,16 +75,16 @@ export class AuthController { if (user) { if (user.mfaEnabled) { - if (!mfaToken && !mfaRecoveryCode) { + if (!mfaCode && !mfaRecoveryCode) { throw new AuthError('MFA Error', 998); } - const isMFATokenValid = await this.mfaService.validateMfa( + const isMFACodeOrMfaRecoveryCodeValid = await this.mfaService.validateMfa( user.id, - mfaToken, + mfaCode, mfaRecoveryCode, ); - if (!isMFATokenValid) { + if (!isMFACodeOrMfaRecoveryCodeValid) { throw new AuthError('Invalid mfa token or recovery code'); } } diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 6cbbda3622cf8..a7fb7235fddd5 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -68,8 +68,8 @@ export class MeController { throw new BadRequestError('Two-factor code is required to change email'); } - const isMfaTokenValid = await this.mfaService.validateMfa(userId, payload.mfaCode, undefined); - if (!isMfaTokenValid) { + const isMfaCodeValid = await this.mfaService.validateMfa(userId, payload.mfaCode, undefined); + if (!isMfaCodeValid) { throw new InvalidMfaCodeError(); } } @@ -142,8 +142,8 @@ export class MeController { throw new BadRequestError('Two-factor code is required to change password.'); } - const isMfaTokenValid = await this.mfaService.validateMfa(user.id, mfaCode, undefined); - if (!isMfaTokenValid) { + const isMfaCodeValid = await this.mfaService.validateMfa(user.id, mfaCode, undefined); + if (!isMfaCodeValid) { throw new InvalidMfaCodeError(); } } diff --git a/packages/cli/src/controllers/mfa.controller.ts b/packages/cli/src/controllers/mfa.controller.ts index 694765761cd25..14125db94dbb5 100644 --- a/packages/cli/src/controllers/mfa.controller.ts +++ b/packages/cli/src/controllers/mfa.controller.ts @@ -59,7 +59,7 @@ export class MFAController { @Post('/enable', { rateLimit: true }) async activateMFA(req: MFA.Activate) { - const { token = null } = req.body; + const { mfaCode = null } = req.body; const { id, mfaEnabled } = req.user; await this.externalHooks.run('mfa.beforeSetup', [req.user]); @@ -67,7 +67,7 @@ export class MFAController { const { decryptedSecret: secret, decryptedRecoveryCodes: recoveryCodes } = await this.mfaService.getSecretAndRecoveryCodes(id); - if (!token) throw new BadRequestError('Token is required to enable MFA feature'); + if (!mfaCode) throw new BadRequestError('MFA code is required to enable MFA feature'); if (mfaEnabled) throw new BadRequestError('MFA already enabled'); @@ -75,10 +75,10 @@ export class MFAController { throw new BadRequestError('Cannot enable MFA without generating secret and recovery codes'); } - const verified = this.mfaService.totp.verifySecret({ secret, token, window: 10 }); + const verified = this.mfaService.totp.verifySecret({ secret, mfaCode, window: 10 }); if (!verified) - throw new BadRequestError('MFA token expired. Close the modal and enable MFA again', 997); + throw new BadRequestError('MFA code expired. Close the modal and enable MFA again', 997); await this.mfaService.enableMfa(id); } @@ -86,27 +86,36 @@ export class MFAController { @Post('/disable', { rateLimit: true }) async disableMFA(req: MFA.Disable) { const { id: userId } = req.user; - const { token = null } = req.body; - if (typeof token !== 'string' || !token) { - throw new BadRequestError('Token is required to disable MFA feature'); + const { mfaCode, mfaRecoveryCode } = req.body; + + const mfaCodeDefined = mfaCode && typeof mfaCode === 'string'; + + const mfaRecoveryCodeDefined = mfaRecoveryCode && typeof mfaRecoveryCode === 'string'; + + if (!mfaCodeDefined && !mfaRecoveryCodeDefined) { + throw new BadRequestError('MFA code or recovery code is required to disable MFA feature'); } - await this.mfaService.disableMfa(userId, token); + if (mfaCodeDefined) { + await this.mfaService.disableMfaWithMfaCode(userId, mfaCode); + } else if (mfaRecoveryCodeDefined) { + await this.mfaService.disableMfaWithRecoveryCode(userId, mfaRecoveryCode); + } } @Post('/verify', { rateLimit: true }) async verifyMFA(req: MFA.Verify) { const { id } = req.user; - const { token } = req.body; + const { mfaCode } = req.body; const { decryptedSecret: secret } = await this.mfaService.getSecretAndRecoveryCodes(id); - if (!token) throw new BadRequestError('Token is required to enable MFA feature'); + if (!mfaCode) throw new BadRequestError('MFA code is required to enable MFA feature'); if (!secret) throw new BadRequestError('No MFA secret se for this user'); - const verified = this.mfaService.totp.verifySecret({ secret, token }); + const verified = this.mfaService.totp.verifySecret({ secret, mfaCode }); if (!verified) throw new BadRequestError('MFA secret could not be verified'); } diff --git a/packages/cli/src/controllers/password-reset.controller.ts b/packages/cli/src/controllers/password-reset.controller.ts index 88155e420a854..6916e01751c99 100644 --- a/packages/cli/src/controllers/password-reset.controller.ts +++ b/packages/cli/src/controllers/password-reset.controller.ts @@ -171,7 +171,7 @@ export class PasswordResetController { */ @Post('/change-password', { skipAuth: true }) async changePassword(req: PasswordResetRequest.NewPassword, res: Response) { - const { token, password, mfaToken } = req.body; + const { token, password, mfaCode } = req.body; if (!token || !password) { this.logger.debug( @@ -189,11 +189,11 @@ export class PasswordResetController { if (!user) throw new NotFoundError(''); if (user.mfaEnabled) { - if (!mfaToken) throw new BadRequestError('If MFA enabled, mfaToken is required.'); + if (!mfaCode) throw new BadRequestError('If MFA enabled, mfaCode is required.'); const { decryptedSecret: secret } = await this.mfaService.getSecretAndRecoveryCodes(user.id); - const validToken = this.mfaService.totp.verifySecret({ secret, token: mfaToken }); + const validToken = this.mfaService.totp.verifySecret({ secret, mfaCode }); if (!validToken) throw new BadRequestError('Invalid MFA token.'); } diff --git a/packages/cli/src/errors/response-errors/invalid-mfa-recovery-code-error.ts b/packages/cli/src/errors/response-errors/invalid-mfa-recovery-code-error.ts new file mode 100644 index 0000000000000..baa0ead8701f5 --- /dev/null +++ b/packages/cli/src/errors/response-errors/invalid-mfa-recovery-code-error.ts @@ -0,0 +1,7 @@ +import { ForbiddenError } from './forbidden.error'; + +export class InvalidMfaRecoveryCodeError extends ForbiddenError { + constructor(hint?: string) { + super('Invalid MFA recovery code', hint); + } +} diff --git a/packages/cli/src/mfa/mfa.service.ts b/packages/cli/src/mfa/mfa.service.ts index 5f730b7bf18d6..84433f5f18919 100644 --- a/packages/cli/src/mfa/mfa.service.ts +++ b/packages/cli/src/mfa/mfa.service.ts @@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid'; import { AuthUserRepository } from '@/databases/repositories/auth-user.repository'; import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error'; +import { InvalidMfaRecoveryCodeError } from '@/errors/response-errors/invalid-mfa-recovery-code-error'; import { TOTPService } from './totp.service'; @@ -56,13 +57,13 @@ export class MfaService { async validateMfa( userId: string, - mfaToken: string | undefined, + mfaCode: string | undefined, mfaRecoveryCode: string | undefined, ) { const user = await this.authUserRepository.findOneByOrFail({ id: userId }); - if (mfaToken) { + if (mfaCode) { const decryptedSecret = this.cipher.decrypt(user.mfaSecret!); - return this.totp.verifySecret({ secret: decryptedSecret, token: mfaToken }); + return this.totp.verifySecret({ secret: decryptedSecret, mfaCode }); } if (mfaRecoveryCode) { @@ -85,12 +86,27 @@ export class MfaService { return await this.authUserRepository.save(user); } - async disableMfa(userId: string, mfaToken: string) { - const isValidToken = await this.validateMfa(userId, mfaToken, undefined); + async disableMfaWithMfaCode(userId: string, mfaCode: string) { + const isValidToken = await this.validateMfa(userId, mfaCode, undefined); + if (!isValidToken) { throw new InvalidMfaCodeError(); } + await this.disableMfaForUser(userId); + } + + async disableMfaWithRecoveryCode(userId: string, recoveryCode: string) { + const isValidToken = await this.validateMfa(userId, undefined, recoveryCode); + + if (!isValidToken) { + throw new InvalidMfaRecoveryCodeError(); + } + + await this.disableMfaForUser(userId); + } + + private async disableMfaForUser(userId: string) { await this.authUserRepository.update(userId, { mfaEnabled: false, mfaSecret: null, diff --git a/packages/cli/src/mfa/totp.service.ts b/packages/cli/src/mfa/totp.service.ts index cbb1f65aacab6..ec9f651635b63 100644 --- a/packages/cli/src/mfa/totp.service.ts +++ b/packages/cli/src/mfa/totp.service.ts @@ -23,10 +23,14 @@ export class TOTPService { }).toString(); } - verifySecret({ secret, token, window = 2 }: { secret: string; token: string; window?: number }) { + verifySecret({ + secret, + mfaCode, + window = 2, + }: { secret: string; mfaCode: string; window?: number }) { return new OTPAuth.TOTP({ secret: OTPAuth.Secret.fromBase32(secret), - }).validate({ token, window }) === null + }).validate({ token: mfaCode, window }) === null ? false : true; } diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index f233d7db46584..7afb1e1bd3c7b 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -228,7 +228,7 @@ export declare namespace PasswordResetRequest { export type NewPassword = AuthlessRequest< {}, {}, - Pick & { token?: string; userId?: string; mfaToken?: string } + Pick & { token?: string; userId?: string; mfaCode?: string } >; } @@ -306,7 +306,7 @@ export type LoginRequest = AuthlessRequest< { email: string; password: string; - mfaToken?: string; + mfaCode?: string; mfaRecoveryCode?: string; } >; @@ -316,9 +316,9 @@ export type LoginRequest = AuthlessRequest< // ---------------------------------- export declare namespace MFA { - type Verify = AuthenticatedRequest<{}, {}, { token: string }, {}>; - type Activate = AuthenticatedRequest<{}, {}, { token: string }, {}>; - type Disable = AuthenticatedRequest<{}, {}, { token: string }, {}>; + type Verify = AuthenticatedRequest<{}, {}, { mfaCode: string }, {}>; + type Activate = AuthenticatedRequest<{}, {}, { mfaCode: string }, {}>; + type Disable = AuthenticatedRequest<{}, {}, { mfaCode?: string; mfaRecoveryCode?: string }, {}>; type Config = AuthenticatedRequest<{}, {}, { login: { enabled: boolean } }, {}>; type ValidateRecoveryCode = AuthenticatedRequest< {}, diff --git a/packages/cli/test/integration/auth.api.test.ts b/packages/cli/test/integration/auth.api.test.ts index 2880526668f30..6c1ddc5892011 100644 --- a/packages/cli/test/integration/auth.api.test.ts +++ b/packages/cli/test/integration/auth.api.test.ts @@ -89,7 +89,7 @@ describe('POST /login', () => { const response = await testServer.authlessAgent.post('/login').send({ email: owner.email, password: ownerPassword, - mfaToken: mfaService.totp.generateTOTP(secret), + mfaCode: mfaService.totp.generateTOTP(secret), }); expect(response.statusCode).toBe(200); diff --git a/packages/cli/test/integration/mfa/mfa.api.test.ts b/packages/cli/test/integration/mfa/mfa.api.test.ts index 3f1963250619c..b67e811646ed1 100644 --- a/packages/cli/test/integration/mfa/mfa.api.test.ts +++ b/packages/cli/test/integration/mfa/mfa.api.test.ts @@ -55,8 +55,8 @@ describe('Enable MFA setup', () => { secondCall.body.data.recoveryCodes.join(''), ); - const token = new TOTPService().generateTOTP(firstCall.body.data.secret); - await testServer.authAgentFor(owner).post('/mfa/disable').send({ token }).expect(200); + const mfaCode = new TOTPService().generateTOTP(firstCall.body.data.secret); + await testServer.authAgentFor(owner).post('/mfa/disable').send({ mfaCode }).expect(200); const thirdCall = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200); @@ -85,21 +85,21 @@ describe('Enable MFA setup', () => { }); test('POST /verify should fail due to invalid MFA token', async () => { - await testServer.authAgentFor(owner).post('/mfa/verify').send({ token: '123' }).expect(400); + await testServer.authAgentFor(owner).post('/mfa/verify').send({ mfaCode: '123' }).expect(400); }); test('POST /verify should fail due to missing token parameter', async () => { await testServer.authAgentFor(owner).get('/mfa/qr').expect(200); - await testServer.authAgentFor(owner).post('/mfa/verify').send({ token: '' }).expect(400); + await testServer.authAgentFor(owner).post('/mfa/verify').send({ mfaCode: '' }).expect(400); }); test('POST /verify should validate MFA token', async () => { const response = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200); const { secret } = response.body.data; - const token = new TOTPService().generateTOTP(secret); + const mfaCode = new TOTPService().generateTOTP(secret); - await testServer.authAgentFor(owner).post('/mfa/verify').send({ token }).expect(200); + await testServer.authAgentFor(owner).post('/mfa/verify').send({ mfaCode }).expect(200); }); }); @@ -109,12 +109,12 @@ describe('Enable MFA setup', () => { }); test('POST /verify should fail due to missing token parameter', async () => { - await testServer.authAgentFor(owner).post('/mfa/verify').send({ token: '' }).expect(400); + await testServer.authAgentFor(owner).post('/mfa/verify').send({ mfaCode: '' }).expect(400); }); test('POST /enable should fail due to invalid MFA token', async () => { await testServer.authAgentFor(owner).get('/mfa/qr').expect(200); - await testServer.authAgentFor(owner).post('/mfa/enable').send({ token: '123' }).expect(400); + await testServer.authAgentFor(owner).post('/mfa/enable').send({ mfaCode: '123' }).expect(400); }); test('POST /enable should fail due to empty secret and recovery codes', async () => { @@ -125,10 +125,10 @@ describe('Enable MFA setup', () => { const response = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200); const { secret } = response.body.data; - const token = new TOTPService().generateTOTP(secret); + const mfaCode = new TOTPService().generateTOTP(secret); - await testServer.authAgentFor(owner).post('/mfa/verify').send({ token }).expect(200); - await testServer.authAgentFor(owner).post('/mfa/enable').send({ token }).expect(200); + await testServer.authAgentFor(owner).post('/mfa/verify').send({ mfaCode }).expect(200); + await testServer.authAgentFor(owner).post('/mfa/enable').send({ mfaCode }).expect(200); const user = await Container.get(AuthUserRepository).findOneOrFail({ where: {}, @@ -145,13 +145,13 @@ describe('Enable MFA setup', () => { const response = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200); const { secret } = response.body.data; - const token = new TOTPService().generateTOTP(secret); + const mfaCode = new TOTPService().generateTOTP(secret); - await testServer.authAgentFor(owner).post('/mfa/verify').send({ token }).expect(200); + await testServer.authAgentFor(owner).post('/mfa/verify').send({ mfaCode }).expect(200); externalHooks.run.mockRejectedValue(new BadRequestError('Error message')); - await testServer.authAgentFor(owner).post('/mfa/enable').send({ token }).expect(400); + await testServer.authAgentFor(owner).post('/mfa/enable').send({ mfaCode }).expect(400); const user = await Container.get(AuthUserRepository).findOneOrFail({ where: {}, @@ -165,13 +165,13 @@ describe('Enable MFA setup', () => { describe('Disable MFA setup', () => { test('POST /disable should disable login with MFA', async () => { const { user, rawSecret } = await createUserWithMfaEnabled(); - const token = new TOTPService().generateTOTP(rawSecret); + const mfaCode = new TOTPService().generateTOTP(rawSecret); await testServer .authAgentFor(user) .post('/mfa/disable') .send({ - token, + mfaCode, }) .expect(200); @@ -184,17 +184,35 @@ describe('Disable MFA setup', () => { expect(dbUser.mfaRecoveryCodes.length).toBe(0); }); - test('POST /disable should fail if invalid token is given', async () => { + test('POST /disable should fail if invalid mfa token is given', async () => { const { user } = await createUserWithMfaEnabled(); await testServer .authAgentFor(user) .post('/mfa/disable') .send({ - token: 'invalid token', + mfaCode: 'invalid token', }) .expect(403); }); + + test('POST /disable should fail if invalid recovery code is given', async () => { + const { user } = await createUserWithMfaEnabled(); + + await testServer + .authAgentFor(user) + .post('/mfa/disable') + .send({ + mfaRecoveryCode: 'invalid token', + }) + .expect(403); + }); + + test('POST /disable should fail if neither MFA code nor recovery code is sent', async () => { + const { user } = await createUserWithMfaEnabled(); + + await testServer.authAgentFor(user).post('/mfa/disable').send({ anotherParam: '' }).expect(400); + }); }); describe('Change password with MFA enabled', () => { @@ -221,7 +239,7 @@ describe('Change password with MFA enabled', () => { .send({ password: newPassword, token: resetPasswordToken, - mfaToken: randomInt(10), + mfaCode: randomInt(10), }) .expect(404); }); @@ -235,14 +253,14 @@ describe('Change password with MFA enabled', () => { const resetPasswordToken = Container.get(AuthService).generatePasswordResetToken(user); - const mfaToken = new TOTPService().generateTOTP(rawSecret); + const mfaCode = new TOTPService().generateTOTP(rawSecret); await testServer.authlessAgent .post('/change-password') .send({ password: newPassword, token: resetPasswordToken, - mfaToken, + mfaCode, }) .expect(200); @@ -252,7 +270,7 @@ describe('Change password with MFA enabled', () => { .send({ email: user.email, password: newPassword, - mfaToken: new TOTPService().generateTOTP(rawSecret), + mfaCode: new TOTPService().generateTOTP(rawSecret), }) .expect(200); @@ -315,7 +333,7 @@ describe('Login', () => { await testServer.authlessAgent .post('/login') - .send({ email: user.email, password: rawPassword, mfaToken: 'wrongvalue' }) + .send({ email: user.email, password: rawPassword, mfaCode: 'wrongvalue' }) .expect(401); }); @@ -333,11 +351,11 @@ describe('Login', () => { test('POST /login should succeed with MFA token', async () => { const { user, rawSecret, rawPassword } = await createUserWithMfaEnabled(); - const token = new TOTPService().generateTOTP(rawSecret); + const mfaCode = new TOTPService().generateTOTP(rawSecret); const response = await testServer.authlessAgent .post('/login') - .send({ email: user.email, password: rawPassword, mfaToken: token }) + .send({ email: user.email, password: rawPassword, mfaCode }) .expect(200); const data = response.body.data; diff --git a/packages/editor-ui/src/api/mfa.ts b/packages/editor-ui/src/api/mfa.ts index 0cce31c96dafd..cce6161269944 100644 --- a/packages/editor-ui/src/api/mfa.ts +++ b/packages/editor-ui/src/api/mfa.ts @@ -11,19 +11,23 @@ export async function getMfaQR( return await makeRestApiRequest(context, 'GET', '/mfa/qr'); } -export async function enableMfa(context: IRestApiContext, data: { token: string }): Promise { +export async function enableMfa( + context: IRestApiContext, + data: { mfaCode: string }, +): Promise { return await makeRestApiRequest(context, 'POST', '/mfa/enable', data); } -export async function verifyMfaToken( +export async function verifyMfaCode( context: IRestApiContext, - data: { token: string }, + data: { mfaCode: string }, ): Promise { return await makeRestApiRequest(context, 'POST', '/mfa/verify', data); } export type DisableMfaParams = { - token: string; + mfaCode?: string; + recoveryCode?: string; }; export async function disableMfa(context: IRestApiContext, data: DisableMfaParams): Promise { diff --git a/packages/editor-ui/src/api/users.ts b/packages/editor-ui/src/api/users.ts index ea14cb6c79501..bff4f65fac9c7 100644 --- a/packages/editor-ui/src/api/users.ts +++ b/packages/editor-ui/src/api/users.ts @@ -21,7 +21,7 @@ export async function loginCurrentUser( export async function login( context: IRestApiContext, - params: { email: string; password: string; mfaToken?: string; mfaRecoveryToken?: string }, + params: { email: string; password: string; mfaCode?: string; mfaRecoveryToken?: string }, ): Promise { return await makeRestApiRequest(context, 'POST', '/login', params); } @@ -84,7 +84,7 @@ export async function validatePasswordToken( export async function changePassword( context: IRestApiContext, - params: { token: string; password: string; mfaToken?: string }, + params: { token: string; password: string; mfaCode?: string }, ): Promise { await makeRestApiRequest(context, 'POST', '/change-password', params); } diff --git a/packages/editor-ui/src/components/MfaSetupModal.vue b/packages/editor-ui/src/components/MfaSetupModal.vue index 9f292124c36bc..44d60348f9f6f 100644 --- a/packages/editor-ui/src/components/MfaSetupModal.vue +++ b/packages/editor-ui/src/components/MfaSetupModal.vue @@ -58,7 +58,7 @@ const onInput = (value: string) => { return; } userStore - .verifyMfaToken({ token: value }) + .verifyMfaCode({ mfaCode: value }) .then(() => { showRecoveryCodes.value = true; authenticatorCode.value = value; @@ -98,7 +98,7 @@ const onDownloadClick = () => { const onSetupClick = async () => { try { - await userStore.enableMfa({ token: authenticatorCode.value }); + await userStore.enableMfa({ mfaCode: authenticatorCode.value }); closeDialog(); toast.showMessage({ type: 'success', diff --git a/packages/editor-ui/src/components/PromptMfaCodeModal/PromptMfaCodeModal.vue b/packages/editor-ui/src/components/PromptMfaCodeModal/PromptMfaCodeModal.vue index 5fdfc205cd6f0..7b367c7d645a3 100644 --- a/packages/editor-ui/src/components/PromptMfaCodeModal/PromptMfaCodeModal.vue +++ b/packages/editor-ui/src/components/PromptMfaCodeModal/PromptMfaCodeModal.vue @@ -6,6 +6,7 @@ import { useI18n } from '@/composables/useI18n'; import { promptMfaCodeBus } from '@/event-bus'; import type { IFormInputs } from '@/Interface'; import { createFormEventBus } from 'n8n-design-system'; +import { validate as validateUuid } from 'uuid'; const i18n = useI18n(); @@ -14,11 +15,11 @@ const readyToSubmit = ref(false); const formFields: IFormInputs = [ { - name: 'mfaCode', + name: 'mfaCodeOrMfaRecoveryCode', initialValue: '', properties: { - label: i18n.baseText('mfa.code.input.label'), - placeholder: i18n.baseText('mfa.code.input.placeholder'), + label: i18n.baseText('mfa.code.recovery.input.label'), + placeholder: i18n.baseText('mfa.code.recovery.input.placeholder'), focusInitially: true, capitalize: true, required: true, @@ -26,9 +27,17 @@ const formFields: IFormInputs = [ }, ]; -function onSubmit(values: { mfaCode: string }) { +function onSubmit(values: { mfaCodeOrMfaRecoveryCode: string }) { + if (validateUuid(values.mfaCodeOrMfaRecoveryCode)) { + promptMfaCodeBus.emit('close', { + mfaRecoveryCode: values.mfaCodeOrMfaRecoveryCode, + }); + + return; + } + promptMfaCodeBus.emit('close', { - mfaCode: values.mfaCode, + mfaCode: values.mfaCodeOrMfaRecoveryCode, }); } @@ -43,7 +52,7 @@ function onFormReady(isReady: boolean) {