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:
parent
77cec73429
commit
de9810709f
6 changed files with 74 additions and 21 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue