mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
test(core): add the mfa binding integration tests (#6330)
* refactor(core): refactor backup code generate flow refactor backup code generate flow * fix(core): fix api payload fix api payload * test(core): implement the mfa binding integration tests implement the mfa binding integration tests * test(core): rebase backup code refactor rebase backup code refactor
This commit is contained in:
parent
556f7e43a7
commit
669279aece
7 changed files with 532 additions and 4 deletions
|
@ -165,7 +165,7 @@ export class Mfa {
|
|||
* @throws {RequestError} with status 400 if the verification record is not verified
|
||||
* @throws {RequestError} with status 400 if the verification record has no secret
|
||||
* @throws {RequestError} with status 404 if the verification record is not found
|
||||
* @throws {RequestError} with status 422 if TOTP is not enabled in the sign-in experience
|
||||
* @throws {RequestError} with status 400 if TOTP is not enabled in the sign-in experience
|
||||
* @throws {RequestError} with status 422 if the user already has a TOTP factor
|
||||
*
|
||||
* - Any existing TOTP factor will be replaced with the new one.
|
||||
|
@ -196,7 +196,7 @@ export class Mfa {
|
|||
* @throws {RequestError} with status 400 if the verification record is not verified
|
||||
* @throws {RequestError} with status 400 if the verification record has no registration data
|
||||
* @throws {RequestError} with status 404 if the verification record is not found
|
||||
* @throws {RequestError} with status 422 if WebAuthn is not enabled in the sign-in experience
|
||||
* @throws {RequestError} with status 400 if WebAuthn is not enabled in the sign-in experience
|
||||
*/
|
||||
async addWebAuthnByVerificationId(verificationId: string) {
|
||||
const verificationRecord = this.interactionContext.getVerificationRecordByTypeAndId(
|
||||
|
@ -215,7 +215,7 @@ export class Mfa {
|
|||
* - Any existing backup code factor will be replaced with the new one.
|
||||
*
|
||||
* @throws {RequestError} with status 404 if no pending backup codes are found
|
||||
* @throws {RequestError} with status 422 if Backup Code is not enabled in the sign-in experience
|
||||
* @throws {RequestError} with status 400 if Backup Code is not enabled in the sign-in experience
|
||||
* @throws {RequestError} with status 422 if the backup code is the only MFA factor
|
||||
*/
|
||||
async addBackupCodeByVerificationId(verificationId: string) {
|
||||
|
@ -241,7 +241,7 @@ export class Mfa {
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws {RequestError} with status 422 if the mfa factors are not enabled in the sign-in experience
|
||||
* @throws {RequestError} with status 400 if the mfa factors are not enabled in the sign-in experience
|
||||
*/
|
||||
async checkAvailability() {
|
||||
const newBindMfaFactors = deduplicate(this.bindMfaFactorsArray.map(({ type }) => type));
|
||||
|
|
|
@ -52,6 +52,41 @@ export default function backupCodeVerificationRoutes<T extends WithLogContext>(
|
|||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${experienceRoutes.verification}/backup-code/generate`,
|
||||
koaGuard({
|
||||
status: [200, 400],
|
||||
response: z.object({
|
||||
verificationId: z.string(),
|
||||
codes: z.array(z.string()),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { experienceInteraction } = ctx;
|
||||
|
||||
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
|
||||
|
||||
const backupCodeVerificationRecord = BackupCodeVerification.create(
|
||||
libraries,
|
||||
queries,
|
||||
experienceInteraction.identifiedUserId
|
||||
);
|
||||
|
||||
const codes = backupCodeVerificationRecord.generate();
|
||||
|
||||
ctx.experienceInteraction.setVerificationRecord(backupCodeVerificationRecord);
|
||||
|
||||
await ctx.experienceInteraction.save();
|
||||
|
||||
ctx.body = {
|
||||
verificationId: backupCodeVerificationRecord.id,
|
||||
codes,
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${experienceRoutes.verification}/backup-code/verify`,
|
||||
koaGuard({
|
||||
|
|
|
@ -4,5 +4,6 @@ export const experienceRoutes = {
|
|||
verification: `${prefix}/verification`,
|
||||
identification: `${prefix}/identification`,
|
||||
profile: `${prefix}/profile`,
|
||||
mfa: `${prefix}/profile/mfa`,
|
||||
prefix,
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
type CreateExperienceApiPayload,
|
||||
type IdentificationApiPayload,
|
||||
type InteractionEvent,
|
||||
type MfaFactor,
|
||||
type NewPasswordIdentityVerificationPayload,
|
||||
type PasswordVerificationPayload,
|
||||
type UpdateProfileApiPayload,
|
||||
|
@ -178,6 +179,14 @@ export class ExperienceClient extends MockClient {
|
|||
.json<{ verificationId: string }>();
|
||||
}
|
||||
|
||||
public async generateMfaBackupCodes() {
|
||||
return api
|
||||
.post(`${experienceRoutes.verification}/backup-code/generate`, {
|
||||
headers: { cookie: this.interactionCookie },
|
||||
})
|
||||
.json<{ verificationId: string; codes: string[] }>();
|
||||
}
|
||||
|
||||
public async verifyBackupCode(payload: { code: string }) {
|
||||
return api
|
||||
.post(`${experienceRoutes.verification}/backup-code/verify`, {
|
||||
|
@ -211,4 +220,17 @@ export class ExperienceClient extends MockClient {
|
|||
json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
public async skipMfaBinding() {
|
||||
return api.post(`${experienceRoutes.mfa}/mfa-skipped`, {
|
||||
headers: { cookie: this.interactionCookie },
|
||||
});
|
||||
}
|
||||
|
||||
public async bindMfa(type: MfaFactor, verificationId: string) {
|
||||
return api.post(`${experienceRoutes.mfa}`, {
|
||||
headers: { cookie: this.interactionCookie },
|
||||
json: { type, verificationId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { authenticator } from 'otplib';
|
||||
|
||||
import { type ExperienceClient } from '#src/client/experience/index.js';
|
||||
|
||||
export const successFullyCreateNewTotpSecret = async (client: ExperienceClient) => {
|
||||
|
@ -23,3 +25,15 @@ export const successfullyVerifyTotp = async (
|
|||
|
||||
return verificationId;
|
||||
};
|
||||
|
||||
export const successfullyCreateAndVerifyTotp = async (client: ExperienceClient) => {
|
||||
const { secret, verificationId } = await successFullyCreateNewTotpSecret(client);
|
||||
const code = authenticator.generate(secret);
|
||||
|
||||
await successfullyVerifyTotp(client, {
|
||||
code,
|
||||
verificationId,
|
||||
});
|
||||
|
||||
return verificationId;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
import { InteractionEvent, MfaFactor, SignInIdentifier } from '@logto/schemas';
|
||||
import { authenticator } from 'otplib';
|
||||
|
||||
import { createUserMfaVerification, deleteUser } from '#src/api/admin-user.js';
|
||||
import { initExperienceClient, logoutClient, processSession } from '#src/helpers/client.js';
|
||||
import {
|
||||
identifyUserWithUsernamePassword,
|
||||
signInWithPassword,
|
||||
} from '#src/helpers/experience/index.js';
|
||||
import {
|
||||
successfullyCreateAndVerifyTotp,
|
||||
successfullyVerifyTotp,
|
||||
} from '#src/helpers/experience/totp-verification.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import {
|
||||
enableAllPasswordSignInMethods,
|
||||
enableMandatoryMfaWithTotp,
|
||||
enableMandatoryMfaWithTotpAndBackupCode,
|
||||
enableUserControlledMfaWithTotp,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile, UserApiTest } from '#src/helpers/user.js';
|
||||
import { devFeatureTest } from '#src/utils.js';
|
||||
|
||||
devFeatureTest.describe('Bind MFA APIs happy path', () => {
|
||||
const userApi = new UserApiTest();
|
||||
|
||||
beforeAll(async () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await userApi.cleanUp();
|
||||
});
|
||||
|
||||
describe('mandatory TOTP', () => {
|
||||
beforeAll(async () => {
|
||||
await enableMandatoryMfaWithTotp();
|
||||
});
|
||||
|
||||
it('should bind TOTP on register', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initExperienceClient();
|
||||
await client.initInteraction({ interactionEvent: InteractionEvent.Register });
|
||||
|
||||
const { verificationId } = await client.createNewPasswordIdentityVerification({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await client.identifyUser({ verificationId });
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'user.missing_mfa',
|
||||
status: 422,
|
||||
});
|
||||
|
||||
const totpVerificationId = await successfullyCreateAndVerifyTotp(client);
|
||||
|
||||
await client.bindMfa(MfaFactor.TOTP, totpVerificationId);
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const userId = await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
|
||||
const signInClient = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(signInClient, username, password);
|
||||
|
||||
await expectRejects(signInClient.submitInteraction(), {
|
||||
code: 'session.mfa.require_mfa_verification',
|
||||
status: 403,
|
||||
});
|
||||
|
||||
await deleteUser(userId);
|
||||
});
|
||||
|
||||
it('should bind TOTP on sign-in', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'user.missing_mfa',
|
||||
status: 422,
|
||||
});
|
||||
|
||||
const totpVerificationId = await successfullyCreateAndVerifyTotp(client);
|
||||
|
||||
await client.bindMfa(MfaFactor.TOTP, totpVerificationId);
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
});
|
||||
|
||||
it('should not throw if user already has TOTP', 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 });
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
});
|
||||
});
|
||||
|
||||
describe('user controlled TOTP', () => {
|
||||
beforeAll(async () => {
|
||||
await enableUserControlledMfaWithTotp();
|
||||
});
|
||||
|
||||
it('should able to skip MFA binding on register', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initExperienceClient();
|
||||
await client.initInteraction({ interactionEvent: InteractionEvent.Register });
|
||||
|
||||
const { verificationId } = await client.createNewPasswordIdentityVerification({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await client.identifyUser({ verificationId });
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'user.missing_mfa',
|
||||
status: 422,
|
||||
});
|
||||
|
||||
await client.skipMfaBinding();
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const userId = await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
|
||||
await signInWithPassword({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await deleteUser(userId);
|
||||
});
|
||||
|
||||
it('should able to skip MFA binding on sign-in', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'user.missing_mfa',
|
||||
status: 422,
|
||||
});
|
||||
|
||||
await client.skipMfaBinding();
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mandatory TOTP with backup codes', () => {
|
||||
beforeAll(async () => {
|
||||
await enableMandatoryMfaWithTotpAndBackupCode();
|
||||
});
|
||||
|
||||
it('should bind TOTP and backup codes on register', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initExperienceClient();
|
||||
await client.initInteraction({ interactionEvent: InteractionEvent.Register });
|
||||
|
||||
const { verificationId } = await client.createNewPasswordIdentityVerification({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await client.identifyUser({ verificationId });
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'user.missing_mfa',
|
||||
status: 422,
|
||||
});
|
||||
|
||||
const totpVerificationId = await successfullyCreateAndVerifyTotp(client);
|
||||
|
||||
await client.bindMfa(MfaFactor.TOTP, totpVerificationId);
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'session.mfa.backup_code_required',
|
||||
status: 422,
|
||||
});
|
||||
|
||||
const { codes, verificationId: backupCodeVerificationId } =
|
||||
await client.generateMfaBackupCodes();
|
||||
|
||||
expect(codes.length).toBeGreaterThan(0);
|
||||
|
||||
await client.bindMfa(MfaFactor.BackupCode, backupCodeVerificationId);
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const userId = await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
|
||||
await deleteUser(userId);
|
||||
});
|
||||
|
||||
it('should bind backup codes on sign-in', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const user = await userApi.create({ username, password });
|
||||
|
||||
const result = await createUserMfaVerification(user.id, MfaFactor.TOTP);
|
||||
|
||||
if (result.type !== MfaFactor.TOTP) {
|
||||
throw new Error('unexpected mfa type');
|
||||
}
|
||||
|
||||
const { secret } = result;
|
||||
const code = authenticator.generate(secret);
|
||||
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'session.mfa.require_mfa_verification',
|
||||
status: 403,
|
||||
});
|
||||
|
||||
await successfullyVerifyTotp(client, { code });
|
||||
|
||||
await expectRejects(client.submitInteraction(), {
|
||||
code: 'session.mfa.backup_code_required',
|
||||
status: 422,
|
||||
});
|
||||
|
||||
const { codes, verificationId } = await client.generateMfaBackupCodes();
|
||||
expect(codes.length).toBeGreaterThan(0);
|
||||
|
||||
await client.bindMfa(MfaFactor.BackupCode, verificationId);
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
import { InteractionEvent, MfaFactor, SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import { createUserMfaVerification } from '#src/api/admin-user.js';
|
||||
import { initExperienceClient } from '#src/helpers/client.js';
|
||||
import { identifyUserWithUsernamePassword } from '#src/helpers/experience/index.js';
|
||||
import { successfullyCreateAndVerifyTotp } from '#src/helpers/experience/totp-verification.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import {
|
||||
enableAllPasswordSignInMethods,
|
||||
enableMandatoryMfaWithTotp,
|
||||
enableMandatoryMfaWithTotpAndBackupCode,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile, UserApiTest } from '#src/helpers/user.js';
|
||||
import { devFeatureTest } from '#src/utils.js';
|
||||
|
||||
devFeatureTest.describe('Bind MFA APIs sad path', () => {
|
||||
const userApi = new UserApiTest();
|
||||
|
||||
beforeAll(async () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await userApi.cleanUp();
|
||||
});
|
||||
|
||||
describe('No MFA is enabled', () => {
|
||||
it('should throw mfa_factor_not_enabled error when binding TOTP', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
const client = await initExperienceClient();
|
||||
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
|
||||
const totpVerificationId = await successfullyCreateAndVerifyTotp(client);
|
||||
|
||||
await expectRejects(client.bindMfa(MfaFactor.TOTP, totpVerificationId), {
|
||||
code: 'session.mfa.mfa_factor_not_enabled',
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mandatory TOTP', () => {
|
||||
beforeAll(async () => {
|
||||
await enableMandatoryMfaWithTotp();
|
||||
});
|
||||
|
||||
it('should throw not supported error when binding TOTP on ForgotPassword interaction', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
const client = await initExperienceClient();
|
||||
await client.initInteraction({ interactionEvent: InteractionEvent.ForgotPassword });
|
||||
|
||||
await expectRejects(client.skipMfaBinding(), {
|
||||
code: 'session.not_supported_for_forgot_password',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
await expectRejects(client.bindMfa(MfaFactor.TOTP, 'dummy_verification_id'), {
|
||||
code: 'session.not_supported_for_forgot_password',
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw identifier_not_found error, if user has not been identified', async () => {
|
||||
const client = await initExperienceClient();
|
||||
await client.initInteraction({ interactionEvent: InteractionEvent.SignIn });
|
||||
await expectRejects(client.bindMfa(MfaFactor.TOTP, 'dummy_verification_id'), {
|
||||
code: 'session.identifier_not_found',
|
||||
status: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw mfa_factor_not_enabled error when trying to bind backup code', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
|
||||
const { verificationId } = await client.generateMfaBackupCodes();
|
||||
|
||||
await expectRejects(client.bindMfa(MfaFactor.BackupCode, verificationId), {
|
||||
code: 'session.mfa.mfa_factor_not_enabled',
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw mfa_policy_not_user_controlled error when trying to skip MFA binding', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
|
||||
await expectRejects(client.skipMfaBinding(), {
|
||||
code: 'session.mfa.mfa_policy_not_user_controlled',
|
||||
status: 422,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mandatory TOTP and Backup Code', () => {
|
||||
beforeAll(async () => {
|
||||
await enableMandatoryMfaWithTotpAndBackupCode();
|
||||
});
|
||||
|
||||
it('should throw if user has a TOTP in record', 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 code = response.codes[0]!;
|
||||
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
await client.verifyBackupCode({ code });
|
||||
|
||||
const totpVerificationId = await successfullyCreateAndVerifyTotp(client);
|
||||
|
||||
await expectRejects(client.bindMfa(MfaFactor.TOTP, totpVerificationId), {
|
||||
code: 'user.totp_already_in_use',
|
||||
status: 422,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if the interaction is not verified, when add new backup codes', 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);
|
||||
const { verificationId } = await client.generateMfaBackupCodes();
|
||||
|
||||
await expectRejects(client.bindMfa(MfaFactor.BackupCode, verificationId), {
|
||||
code: 'session.mfa.require_mfa_verification',
|
||||
status: 403,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if the backup codes is the only MFA factor', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
|
||||
const { verificationId } = await client.generateMfaBackupCodes();
|
||||
|
||||
await expectRejects(client.bindMfa(MfaFactor.BackupCode, verificationId), {
|
||||
code: 'session.mfa.backup_code_can_not_be_alone',
|
||||
status: 422,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if no pending backup codes is found', async () => {
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
await userApi.create({ username, password });
|
||||
|
||||
const client = await initExperienceClient();
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
const totpVerificationId = await successfullyCreateAndVerifyTotp(client);
|
||||
await client.bindMfa(MfaFactor.TOTP, totpVerificationId);
|
||||
|
||||
await expectRejects(client.bindMfa(MfaFactor.BackupCode, 'invalid_verification'), {
|
||||
code: 'session.verification_session_not_found',
|
||||
status: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue