Skip to content

Commit

Permalink
test(core): add the mfa binding integration tests (#6330)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
simeng-li authored Jul 26, 2024
1 parent 556f7e4 commit 669279a
Show file tree
Hide file tree
Showing 7 changed files with 532 additions and 4 deletions.
8 changes: 4 additions & 4 deletions packages/core/src/routes/experience/classes/mfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions packages/integration-tests/src/client/experience/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export const experienceRoutes = {
verification: `${prefix}/verification`,
identification: `${prefix}/identification`,
profile: `${prefix}/profile`,
mfa: `${prefix}/profile/mfa`,
prefix,
};
22 changes: 22 additions & 0 deletions packages/integration-tests/src/client/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type CreateExperienceApiPayload,
type IdentificationApiPayload,
type InteractionEvent,
type MfaFactor,
type NewPasswordIdentityVerificationPayload,
type PasswordVerificationPayload,
type UpdateProfileApiPayload,
Expand Down Expand Up @@ -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`, {
Expand Down Expand Up @@ -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 },
});
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { authenticator } from 'otplib';

import { type ExperienceClient } from '#src/client/experience/index.js';

export const successFullyCreateNewTotpSecret = async (client: ExperienceClient) => {
Expand All @@ -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;
};
Loading

0 comments on commit 669279a

Please sign in to comment.