0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor(experience): use last used factor when verifing mfa factors (#4775)

This commit is contained in:
Xiao Yijun 2023-10-27 16:59:48 +08:00 committed by GitHub
parent 77cec73429
commit de9810709f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 21 deletions

View file

@ -28,6 +28,19 @@ const useMfaErrorHandler = ({ replace }: Options = {}) => {
(flow: UserMfaFlow, state: MfaFlowState) => {
const { availableFactors } = state;
if (flow === UserMfaFlow.MfaVerification) {
// In MFA verification flow, the user will be redirected to the last used factor which is the first one in the array.
const factor = availableFactors[0];
if (!factor) {
return;
}
navigate({ pathname: `/${flow}/${factor}` }, { replace, state });
return;
}
// Binding flow
if (availableFactors.length > 1) {
navigate({ pathname: `/${flow}` }, { replace, state });
return;
@ -39,7 +52,7 @@ const useMfaErrorHandler = ({ replace }: Options = {}) => {
return;
}
if (factor === MfaFactor.TOTP && flow === UserMfaFlow.MfaBinding) {
if (factor === MfaFactor.TOTP) {
void startTotpBinding(state);
return;
}

View file

@ -9,8 +9,8 @@ import {
enableMandatoryMfaWithWebAuthnAndBackupCode,
resetMfaSettings,
} from '#src/helpers/sign-in-experience.js';
import ExpectBackupCodeExperience from '#src/ui-helpers/expect-backup-code-experience.js';
import { generateUsername, waitFor } from '#src/utils.js';
import ExpectWebAuthnExperience from '#src/ui-helpers/expect-webauthn-experience.js';
import { generateUsername } from '#src/utils.js';
describe('MFA - Backup Code', () => {
beforeAll(async () => {
@ -43,7 +43,7 @@ describe('MFA - Backup Code', () => {
const password = 'l0gt0_T3st_P@ssw0rd';
it('should bind backup codes when registering and verify backup codes when signing in', async () => {
const experience = new ExpectBackupCodeExperience(await browser.newPage());
const experience = new ExpectWebAuthnExperience(await browser.newPage());
await experience.setupVirtualAuthenticator();
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('identifier', username, { submit: true });
@ -68,9 +68,15 @@ describe('MFA - Backup Code', () => {
},
{ submit: true }
);
// Wait for the page to process submitting request.
await waitFor(500);
await experience.page.waitForNetworkIdle();
// Should navigate to the WebAuthn verification page
experience.toBeAt('mfa-verification/WebAuthn');
// Nav back to the list page
await experience.toClickSwitchFactorsLink({ isBinding: false });
experience.toBeAt('mfa-verification');
// Select backup code
await experience.toClick('button', 'Backup code');
experience.toBeAt('mfa-verification/BackupCode');
await experience.toFillInput('code', backupCodes.at(0) ?? '', { submit: true });

View file

@ -7,10 +7,10 @@ import { demoAppUrl } from '#src/constants.js';
import { clearConnectorsByTypes, setSocialConnector } from '#src/helpers/connector.js';
import { resetMfaSettings } from '#src/helpers/sign-in-experience.js';
import { generateNewUser } from '#src/helpers/user.js';
import ExpectTotpExperience from '#src/ui-helpers/expect-totp-experience.js';
import ExpectWebAuthnExperience from '#src/ui-helpers/expect-webauthn-experience.js';
import { waitFor } from '#src/utils.js';
describe('MFA - Factor switching', () => {
describe('MFA - Multi factors', () => {
beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms, ConnectorType.Social]);
await setSocialConnector();
@ -41,12 +41,12 @@ describe('MFA - Factor switching', () => {
await resetMfaSettings();
});
it('should be able to switch between different factors', async () => {
it('should be able to complete MFA flow with multi factors', async () => {
const { userProfile, user } = await generateNewUser({ username: true, password: true });
const experience = new ExpectTotpExperience(await browser.newPage());
const experience = new ExpectWebAuthnExperience(await browser.newPage());
await experience.startWith(demoAppUrl, 'sign-in');
await experience.setupVirtualAuthenticator();
await experience.toFillForm(
{
identifier: userProfile.username,
@ -65,22 +65,48 @@ describe('MFA - Factor switching', () => {
experience.toBeAt('mfa-binding/Totp');
// Navigate back
await experience.toClick('a[class*=switchLink');
await experience.toClickSwitchFactorsLink({ isBinding: true });
experience.toBeAt('mfa-binding');
await expect(experience.page).toMatchElement('div[class$=title]', {
text: 'Add 2-step authentication',
});
// Select WebAuthn
// Switch to WebAuthn
await experience.toClick('button div[class$=name]', 'Passkey');
experience.toBeAt('mfa-binding/WebAuthn');
await experience.toClick('a[class*=switchLink');
await experience.toClickSwitchFactorsLink({ isBinding: true });
experience.toBeAt('mfa-binding');
await expect(experience.page).toMatchElement('div[class$=title]', {
text: 'Add 2-step authentication',
});
await experience.page.close();
// Bind WebAuthn
await experience.toClick('button div[class$=name]', 'Passkey');
// Wait the WebAuthn to be prepared
await experience.page.waitForNetworkIdle();
experience.toBeAt('mfa-binding/WebAuthn');
await experience.toCreatePasskey();
// Backup codes page
await experience.toClick('button', 'Continue');
await experience.verifyThenEnd(false);
// Sign in with latest used factor
await experience.startWith(demoAppUrl, 'sign-in');
await experience.toFillForm(
{
identifier: userProfile.username,
password: userProfile.password,
},
{ submit: true }
);
await experience.page.waitForNetworkIdle();
experience.toBeAt('mfa-verification/WebAuthn');
await experience.toVerifyViaPasskey();
await experience.clearVirtualAuthenticator();
await experience.verifyThenEnd();
await deleteUser(user.id);
});
});

View file

@ -1,10 +1,11 @@
import { cls } from '#src/utils.js';
import ExpectWebAuthnExperience from './expect-webauthn-experience.js';
import ExpectExperience from './expect-experience.js';
/**
* Note: The backup code tests are based on the WebAuthn experience flow since the backup code factor cannot be enabled alone.
*/
export default class ExpectBackupCodeExperience extends ExpectWebAuthnExperience {
export default class ExpectMfaExperience extends ExpectExperience {
constructor(thePage = global.page) {
super(thePage);
}
@ -22,4 +23,11 @@ export default class ExpectBackupCodeExperience extends ExpectWebAuthnExperience
})
);
}
async toClickSwitchFactorsLink({ isBinding }: { isBinding: boolean }) {
await this.toClick(
'a',
isBinding ? 'Link another 2-step authentication' : 'Try another method to verify'
);
}
}

View file

@ -2,9 +2,9 @@ import { authenticator } from 'otplib';
import { waitFor, dcls } from '#src/utils.js';
import ExpectExperience from './expect-experience.js';
import ExpectMfaExperience from './expect-mfa-experience.js';
export default class ExpectTotpExperience extends ExpectExperience {
export default class ExpectTotpExperience extends ExpectMfaExperience {
constructor(thePage = global.page) {
super(thePage);
}

View file

@ -1,8 +1,8 @@
import { type CDPSession } from 'puppeteer';
import ExpectExperience from './expect-experience.js';
import ExpectMfaExperience from './expect-mfa-experience.js';
export default class ExpectWebAuthnExperience extends ExpectExperience {
export default class ExpectWebAuthnExperience extends ExpectMfaExperience {
private authenticatorId?: string;
private _cdpClient?: CDPSession;