-
-
Notifications
You must be signed in to change notification settings - Fork 423
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(core): implement the mfa binding integration tests
implement the mfa binding integration tests
- Loading branch information
Showing
6 changed files
with
495 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
274 changes: 274 additions & 0 deletions
274
packages/integration-tests/src/tests/api/experience-api/bind-mfa/happpy-path.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
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 } = await client.generateMfaBackupCodes(); | ||
|
||
expect(codes.length).toBeGreaterThan(0); | ||
|
||
await client.bindBackupCodes(); | ||
|
||
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 } = await client.generateMfaBackupCodes(); | ||
expect(codes.length).toBeGreaterThan(0); | ||
|
||
await client.bindBackupCodes(); | ||
|
||
const { redirectTo } = await client.submitInteraction(); | ||
await processSession(client, redirectTo); | ||
await logoutClient(client); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.