mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): add mfa verification guard (#6262)
add mfa verification guard
This commit is contained in:
parent
c93ffb4760
commit
7537c510f7
10 changed files with 299 additions and 18 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { type ToZodObject } from '@logto/connector-kit';
|
import { type ToZodObject } from '@logto/connector-kit';
|
||||||
import { InteractionEvent, type VerificationType } from '@logto/schemas';
|
import { InteractionEvent, type User, type VerificationType } from '@logto/schemas';
|
||||||
import { generateStandardId } from '@logto/shared';
|
import { generateStandardId } from '@logto/shared';
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { conditional } from '@silverhand/essentials';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -17,6 +17,7 @@ import {
|
||||||
identifyUserByVerificationRecord,
|
identifyUserByVerificationRecord,
|
||||||
toUserSocialIdentityData,
|
toUserSocialIdentityData,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
import { MfaValidator } from './validators/mfa-validator.js';
|
||||||
import { ProfileValidator } from './validators/profile-validator.js';
|
import { ProfileValidator } from './validators/profile-validator.js';
|
||||||
import { SignInExperienceValidator } from './validators/sign-in-experience-validator.js';
|
import { SignInExperienceValidator } from './validators/sign-in-experience-validator.js';
|
||||||
import {
|
import {
|
||||||
|
@ -55,6 +56,7 @@ export default class ExperienceInteraction {
|
||||||
private readonly verificationRecords = new Map<VerificationType, VerificationRecord>();
|
private readonly verificationRecords = new Map<VerificationType, VerificationRecord>();
|
||||||
/** The userId of the user for the current interaction. Only available once the user is identified. */
|
/** The userId of the user for the current interaction. Only available once the user is identified. */
|
||||||
private userId?: string;
|
private userId?: string;
|
||||||
|
private userCache?: User;
|
||||||
/** The user provided profile data in the current interaction that needs to be stored to database. */
|
/** The user provided profile data in the current interaction that needs to be stored to database. */
|
||||||
#profile?: InteractionProfile;
|
#profile?: InteractionProfile;
|
||||||
/** The interaction event for the current interaction. */
|
/** The interaction event for the current interaction. */
|
||||||
|
@ -190,6 +192,27 @@ export default class ExperienceInteraction {
|
||||||
return this.verificationRecordsArray.find((record) => record.id === verificationId);
|
return this.verificationRecordsArray.find((record) => record.id === verificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the interaction verification records against the sign-in experience and user MFA settings.
|
||||||
|
*
|
||||||
|
* The interaction is verified if at least one user enabled MFA verification record is present and verified.
|
||||||
|
*/
|
||||||
|
public async guardMfaVerificationStatus() {
|
||||||
|
const user = await this.getIdentifiedUser();
|
||||||
|
const mfaSettings = await this.signInExperienceValidator.getMfaSettings();
|
||||||
|
const mfaValidator = new MfaValidator(mfaSettings, user);
|
||||||
|
|
||||||
|
const isVerified = mfaValidator.isMfaVerified(this.verificationRecordsArray);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
isVerified,
|
||||||
|
new RequestError(
|
||||||
|
{ code: 'session.mfa.require_mfa_verification', status: 403 },
|
||||||
|
{ availableFactors: mfaValidator.availableMfaVerificationTypes }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Save the current interaction result. */
|
/** Save the current interaction result. */
|
||||||
public async save() {
|
public async save() {
|
||||||
const { provider } = this.tenant;
|
const { provider } = this.tenant;
|
||||||
|
@ -212,22 +235,30 @@ export default class ExperienceInteraction {
|
||||||
|
|
||||||
/** Submit the current interaction result to the OIDC provider and clear the interaction data */
|
/** Submit the current interaction result to the OIDC provider and clear the interaction data */
|
||||||
public async submit() {
|
public async submit() {
|
||||||
assertThat(this.userId, 'session.verification_session_not_found');
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
queries: { users: userQueries, userSsoIdentities: userSsoIdentitiesQueries },
|
queries: { users: userQueries, userSsoIdentities: userSsoIdentitiesQueries },
|
||||||
} = this.tenant;
|
} = this.tenant;
|
||||||
|
|
||||||
const user = await userQueries.findUserById(this.userId);
|
// Initiated
|
||||||
|
assertThat(
|
||||||
|
this.interactionEvent,
|
||||||
|
new RequestError({ code: 'session.interaction_not_found', status: 404 })
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: mfa validation
|
// Identified
|
||||||
// TODO: profile updates validation
|
const user = await this.getIdentifiedUser();
|
||||||
// TODO: missing profile fields validation
|
|
||||||
|
// Verified
|
||||||
|
if (this.#interactionEvent !== InteractionEvent.ForgotPassword) {
|
||||||
|
await this.guardMfaVerificationStatus();
|
||||||
|
}
|
||||||
|
|
||||||
const { socialIdentity, enterpriseSsoIdentity, ...rest } = this.#profile ?? {};
|
const { socialIdentity, enterpriseSsoIdentity, ...rest } = this.#profile ?? {};
|
||||||
|
|
||||||
|
// TODO: profile updates validation
|
||||||
|
|
||||||
// Update user profile
|
// Update user profile
|
||||||
await userQueries.updateUserById(this.userId, {
|
await userQueries.updateUserById(user.id, {
|
||||||
...rest,
|
...rest,
|
||||||
...conditional(
|
...conditional(
|
||||||
socialIdentity && {
|
socialIdentity && {
|
||||||
|
@ -240,6 +271,8 @@ export default class ExperienceInteraction {
|
||||||
lastSignInAt: Date.now(),
|
lastSignInAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: missing profile fields validation
|
||||||
|
|
||||||
if (enterpriseSsoIdentity) {
|
if (enterpriseSsoIdentity) {
|
||||||
await userSsoIdentitiesQueries.insert({
|
await userSsoIdentitiesQueries.insert({
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
|
@ -254,7 +287,7 @@ export default class ExperienceInteraction {
|
||||||
const { provider } = this.tenant;
|
const { provider } = this.tenant;
|
||||||
|
|
||||||
const redirectTo = await provider.interactionResult(this.ctx.req, this.ctx.res, {
|
const redirectTo = await provider.interactionResult(this.ctx.req, this.ctx.res, {
|
||||||
login: { accountId: this.userId },
|
login: { accountId: user.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ctx.body = { redirectTo };
|
this.ctx.body = { redirectTo };
|
||||||
|
@ -377,4 +410,28 @@ export default class ExperienceInteraction {
|
||||||
...profile,
|
...profile,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the interaction is identified and return the identified user.
|
||||||
|
*
|
||||||
|
* @throws RequestError with 400 if the user is not identified
|
||||||
|
* @throws RequestError with 404 if the user is not found
|
||||||
|
*/
|
||||||
|
private async getIdentifiedUser(): Promise<User> {
|
||||||
|
if (this.userCache) {
|
||||||
|
return this.userCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identified
|
||||||
|
assertThat(this.userId, 'session.identifier_not_found');
|
||||||
|
|
||||||
|
const {
|
||||||
|
queries: { users: userQueries },
|
||||||
|
} = this.tenant;
|
||||||
|
|
||||||
|
const user = await userQueries.findUserById(this.userId);
|
||||||
|
|
||||||
|
this.userCache = user;
|
||||||
|
return this.userCache;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import {
|
||||||
|
MfaFactor,
|
||||||
|
VerificationType,
|
||||||
|
type Mfa,
|
||||||
|
type MfaVerification,
|
||||||
|
type User,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type VerificationRecord } from '../verifications/index.js';
|
||||||
|
|
||||||
|
const mfaVerificationTypes = Object.freeze([
|
||||||
|
VerificationType.TOTP,
|
||||||
|
VerificationType.BackupCode,
|
||||||
|
VerificationType.WebAuthn,
|
||||||
|
]);
|
||||||
|
|
||||||
|
type MfaVerificationType =
|
||||||
|
| VerificationType.TOTP
|
||||||
|
| VerificationType.BackupCode
|
||||||
|
| VerificationType.WebAuthn;
|
||||||
|
|
||||||
|
const mfaVerificationTypeToMfaFactorMap = Object.freeze({
|
||||||
|
[VerificationType.TOTP]: MfaFactor.TOTP,
|
||||||
|
[VerificationType.BackupCode]: MfaFactor.BackupCode,
|
||||||
|
[VerificationType.WebAuthn]: MfaFactor.WebAuthn,
|
||||||
|
}) satisfies Record<MfaVerificationType, MfaFactor>;
|
||||||
|
|
||||||
|
const isMfaVerificationRecordType = (type: VerificationType): type is MfaVerificationType => {
|
||||||
|
return mfaVerificationTypes.includes(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MfaValidator {
|
||||||
|
constructor(
|
||||||
|
private readonly mfaSettings: Mfa,
|
||||||
|
private readonly user: User
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the enabled MFA factors for the user
|
||||||
|
* - Filter out MFA factors that are not configured in the sign-in experience
|
||||||
|
*/
|
||||||
|
get userEnabledMfaVerifications() {
|
||||||
|
const { mfaVerifications } = this.user;
|
||||||
|
|
||||||
|
return mfaVerifications.filter((verification) =>
|
||||||
|
this.mfaSettings.factors.includes(verification.type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For front-end display usage only
|
||||||
|
* Return the available MFA verifications for the user.
|
||||||
|
*/
|
||||||
|
get availableMfaVerificationTypes() {
|
||||||
|
return (
|
||||||
|
this.userEnabledMfaVerifications
|
||||||
|
// Filter out backup codes if all the codes are used
|
||||||
|
.filter((verification) => {
|
||||||
|
if (verification.type !== MfaFactor.BackupCode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return verification.codes.some((code) => !code.usedAt);
|
||||||
|
})
|
||||||
|
// Filter out duplicated verifications with the same type
|
||||||
|
.reduce<MfaVerification[]>((verifications, verification) => {
|
||||||
|
if (verifications.some(({ type }) => type === verification.type)) {
|
||||||
|
return verifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...verifications, verification];
|
||||||
|
}, [])
|
||||||
|
.slice()
|
||||||
|
// Sort by last used time, the latest used factor is the first one, backup code is always the last one
|
||||||
|
.sort((verificationA, verificationB) => {
|
||||||
|
if (verificationA.type === MfaFactor.BackupCode) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verificationB.type === MfaFactor.BackupCode) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Date(verificationB.lastUsedAt ?? 0).getTime() -
|
||||||
|
new Date(verificationA.lastUsedAt ?? 0).getTime()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(({ type }) => type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isMfaEnabled() {
|
||||||
|
return this.userEnabledMfaVerifications.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMfaVerified(verificationRecords: VerificationRecord[]) {
|
||||||
|
// MFA validation is not enabled
|
||||||
|
if (!this.isMfaEnabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mfaVerificationRecords = this.filterVerifiedMfaVerificationRecords(verificationRecords);
|
||||||
|
|
||||||
|
return mfaVerificationRecords.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterVerifiedMfaVerificationRecords(verificationRecords: VerificationRecord[]) {
|
||||||
|
const enabledMfaFactors = this.userEnabledMfaVerifications;
|
||||||
|
|
||||||
|
// Filter out the verified MFA verification records
|
||||||
|
const mfaVerificationRecords = verificationRecords.filter(({ type, isVerified }) => {
|
||||||
|
return (
|
||||||
|
isVerified &&
|
||||||
|
isMfaVerificationRecordType(type) &&
|
||||||
|
// Check if the verification type is enabled in the user's MFA settings
|
||||||
|
enabledMfaFactors.some((factor) => factor.type === mfaVerificationTypeToMfaFactorMap[type])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return mfaVerificationRecords;
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,6 +111,12 @@ export class SignInExperienceValidator {
|
||||||
return availableSsoConnectors.filter(({ domains }) => domains.includes(domain));
|
return availableSsoConnectors.filter(({ domains }) => domains.includes(domain));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getMfaSettings() {
|
||||||
|
const { mfa } = await this.getSignInExperienceData();
|
||||||
|
|
||||||
|
return mfa;
|
||||||
|
}
|
||||||
|
|
||||||
public async getSignInExperienceData() {
|
public async getSignInExperienceData() {
|
||||||
this.signInExperienceDataCache ||=
|
this.signInExperienceDataCache ||=
|
||||||
await this.queries.signInExperiences.findDefaultSignInExperience();
|
await this.queries.signInExperiences.findDefaultSignInExperience();
|
||||||
|
|
|
@ -124,6 +124,7 @@ export class TotpVerification implements VerificationRecord<VerificationType.TOT
|
||||||
|
|
||||||
const { mfaVerifications } = await findUserById(this.userId);
|
const { mfaVerifications } = await findUserById(this.userId);
|
||||||
|
|
||||||
|
// User can only have one TOTP MFA record in the profile
|
||||||
const totpVerification = findUserTotp(mfaVerifications);
|
const totpVerification = findUserTotp(mfaVerifications);
|
||||||
|
|
||||||
// Can not found totp verification, this is an invalid request, throw invalid code error anyway for security reason
|
// Can not found totp verification, this is an invalid request, throw invalid code error anyway for security reason
|
||||||
|
|
|
@ -128,7 +128,7 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
|
||||||
router.post(
|
router.post(
|
||||||
`${experienceRoutes.prefix}/submit`,
|
`${experienceRoutes.prefix}/submit`,
|
||||||
koaGuard({
|
koaGuard({
|
||||||
status: [200, 400],
|
status: [200, 400, 403, 422],
|
||||||
response: z.object({
|
response: z.object({
|
||||||
redirectTo: z.string(),
|
redirectTo: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -32,8 +32,6 @@ export default function backupCodeVerificationRoutes<T extends WithLogContext>(
|
||||||
|
|
||||||
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
|
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
|
||||||
|
|
||||||
// TODO: Check if the MFA is enabled
|
|
||||||
|
|
||||||
const backupCodeVerificationRecord = BackupCodeVerification.create(
|
const backupCodeVerificationRecord = BackupCodeVerification.create(
|
||||||
libraries,
|
libraries,
|
||||||
queries,
|
queries,
|
||||||
|
@ -49,6 +47,8 @@ export default function backupCodeVerificationRoutes<T extends WithLogContext>(
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
verificationId: backupCodeVerificationRecord.id,
|
verificationId: backupCodeVerificationRecord.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return next();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,10 @@ import assertThat from '#src/utils/assert-that.js';
|
||||||
import { type WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
import { type WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
||||||
import { type WithInteractionSieContext } from '../middleware/koa-interaction-sie.js';
|
import { type WithInteractionSieContext } from '../middleware/koa-interaction-sie.js';
|
||||||
import {
|
import {
|
||||||
type VerifiedSignInInteractionResult,
|
type AccountVerifiedInteractionResult,
|
||||||
type VerifiedInteractionResult,
|
type VerifiedInteractionResult,
|
||||||
type VerifiedRegisterInteractionResult,
|
type VerifiedRegisterInteractionResult,
|
||||||
type AccountVerifiedInteractionResult,
|
type VerifiedSignInInteractionResult,
|
||||||
} from '../types/index.js';
|
} from '../types/index.js';
|
||||||
import { generateBackupCodes } from '../utils/backup-code-validation.js';
|
import { generateBackupCodes } from '../utils/backup-code-validation.js';
|
||||||
import { storeInteractionResult } from '../utils/interaction.js';
|
import { storeInteractionResult } from '../utils/interaction.js';
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const successFullyCreateNewTotpSecret = async (client: ExperienceClient)
|
||||||
return { secret, secretQrCode, verificationId };
|
return { secret, secretQrCode, verificationId };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const successFullyVerifyTotp = async (
|
export const successfullyVerifyTotp = async (
|
||||||
client: ExperienceClient,
|
client: ExperienceClient,
|
||||||
payload: {
|
payload: {
|
||||||
code: string;
|
code: string;
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { MfaFactor } from '@logto/schemas';
|
||||||
|
import { authenticator } from 'otplib';
|
||||||
|
|
||||||
|
import { createUserMfaVerification } from '#src/api/admin-user.js';
|
||||||
|
import { initExperienceClient } from '#src/helpers/client.js';
|
||||||
|
import { identifyUserWithUsernamePassword } from '#src/helpers/experience/index.js';
|
||||||
|
import { successfullyVerifyTotp } from '#src/helpers/experience/totp-verification.js';
|
||||||
|
import { expectRejects } from '#src/helpers/index.js';
|
||||||
|
import {
|
||||||
|
enableAllPasswordSignInMethods,
|
||||||
|
enableMandatoryMfaWithTotpAndBackupCode,
|
||||||
|
} from '#src/helpers/sign-in-experience.js';
|
||||||
|
import { generateNewUserProfile, UserApiTest } from '#src/helpers/user.js';
|
||||||
|
import { devFeatureTest } from '#src/utils.js';
|
||||||
|
|
||||||
|
devFeatureTest.describe('mfa sign-in verification', () => {
|
||||||
|
const userApi = new UserApiTest();
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await enableAllPasswordSignInMethods();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userApi.cleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TOTP verification', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await enableMandatoryMfaWithTotpAndBackupCode();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw require_mfa_verification error when signing in without mfa verification', async () => {
|
||||||
|
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||||
|
|
||||||
|
const user = await userApi.create({ username, password });
|
||||||
|
await createUserMfaVerification(user.id, MfaFactor.TOTP);
|
||||||
|
|
||||||
|
const client = await initExperienceClient();
|
||||||
|
|
||||||
|
await identifyUserWithUsernamePassword(client, username, password);
|
||||||
|
|
||||||
|
await expectRejects(client.submitInteraction(), {
|
||||||
|
code: 'session.mfa.require_mfa_verification',
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sign-in successfully with TOTP verification', async () => {
|
||||||
|
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||||
|
const user = await userApi.create({ username, password });
|
||||||
|
|
||||||
|
const response = await createUserMfaVerification(user.id, MfaFactor.TOTP);
|
||||||
|
|
||||||
|
if (response.type !== MfaFactor.TOTP) {
|
||||||
|
throw new Error('unexpected mfa type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { secret } = response;
|
||||||
|
|
||||||
|
const client = await initExperienceClient();
|
||||||
|
|
||||||
|
await identifyUserWithUsernamePassword(client, username, password);
|
||||||
|
|
||||||
|
const code = authenticator.generate(secret);
|
||||||
|
|
||||||
|
await successfullyVerifyTotp(client, { code });
|
||||||
|
|
||||||
|
await expect(client.submitInteraction()).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sign-in successfully with backup code with both TOTP and backup code enabled', async () => {
|
||||||
|
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||||
|
const user = await userApi.create({ username, password });
|
||||||
|
|
||||||
|
await createUserMfaVerification(user.id, MfaFactor.TOTP);
|
||||||
|
const response = await createUserMfaVerification(user.id, MfaFactor.BackupCode);
|
||||||
|
|
||||||
|
if (response.type !== MfaFactor.BackupCode) {
|
||||||
|
throw new Error('unexpected mfa type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { codes } = response;
|
||||||
|
|
||||||
|
const client = await initExperienceClient();
|
||||||
|
|
||||||
|
await identifyUserWithUsernamePassword(client, username, password);
|
||||||
|
|
||||||
|
const code = codes[0]!;
|
||||||
|
|
||||||
|
await client.verifyBackupCode({ code });
|
||||||
|
|
||||||
|
await expect(client.submitInteraction()).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,7 +6,7 @@ import { initExperienceClient } from '#src/helpers/client.js';
|
||||||
import { identifyUserWithUsernamePassword } from '#src/helpers/experience/index.js';
|
import { identifyUserWithUsernamePassword } from '#src/helpers/experience/index.js';
|
||||||
import {
|
import {
|
||||||
successFullyCreateNewTotpSecret,
|
successFullyCreateNewTotpSecret,
|
||||||
successFullyVerifyTotp,
|
successfullyVerifyTotp,
|
||||||
} from '#src/helpers/experience/totp-verification.js';
|
} from '#src/helpers/experience/totp-verification.js';
|
||||||
import { expectRejects } from '#src/helpers/index.js';
|
import { expectRejects } from '#src/helpers/index.js';
|
||||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||||
|
@ -98,7 +98,7 @@ devFeatureTest.describe('TOTP verification APIs', () => {
|
||||||
const { verificationId, secret } = await successFullyCreateNewTotpSecret(client);
|
const { verificationId, secret } = await successFullyCreateNewTotpSecret(client);
|
||||||
const code = authenticator.generate(secret);
|
const code = authenticator.generate(secret);
|
||||||
|
|
||||||
await successFullyVerifyTotp(client, { code, verificationId });
|
await successfullyVerifyTotp(client, { code, verificationId });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ devFeatureTest.describe('TOTP verification APIs', () => {
|
||||||
|
|
||||||
const code = authenticator.generate(secret);
|
const code = authenticator.generate(secret);
|
||||||
|
|
||||||
await successFullyVerifyTotp(client, { code });
|
await successfullyVerifyTotp(client, { code });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue