0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

test: add tests for mfa totp flows (#4566)

This commit is contained in:
wangsijie 2023-09-26 14:42:02 +08:00 committed by GitHub
parent a8e8703898
commit c3f865ac2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 188 additions and 2 deletions

View file

@ -53,5 +53,8 @@
"eslintConfig": {
"extends": "@silverhand"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
"prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": {
"otplib": "^12.0.1"
}
}

View file

@ -3,6 +3,7 @@ import type {
IdentifierPayload,
Profile,
RequestVerificationCodePayload,
BindMfaPayload,
} from '@logto/schemas';
import type { Got } from 'got';
@ -67,6 +68,15 @@ export const putInteractionProfile = async (cookie: string, payload: Profile) =>
})
.json();
export const putInteractionBindMfa = async (cookie: string, payload: BindMfaPayload) =>
api
.put('interaction/bind-mfa', {
headers: { cookie },
json: payload,
followRedirect: false,
})
.json();
export const deleteInteractionProfile = async (cookie: string) =>
api
.delete('interaction/profile', {
@ -106,6 +116,15 @@ export const createSocialAuthorizationUri = async (
followRedirect: false,
});
export const initTotp = async (cookie: string) =>
api
.post('interaction/verification/totp', {
headers: { cookie },
json: {},
followRedirect: false,
})
.json<{ secret: string }>();
export const consent = async (api: Got, cookie: string) =>
api
.post('interaction/consent', {

View file

@ -1,5 +1,5 @@
import type { SignInExperience } from '@logto/schemas';
import { SignInMode, SignInIdentifier } from '@logto/schemas';
import { SignInMode, SignInIdentifier, MfaFactor, MfaPolicy } from '@logto/schemas';
import { updateSignInExperience } from '#src/api/index.js';
@ -60,6 +60,7 @@ export const enableAllPasswordSignInMethods = async (
signIn: {
methods: defaultPasswordSignInMethods,
},
mfa: { factors: [], policy: MfaPolicy.UserControlled },
});
export const enableAllVerificationCodeSignInMethods = async (
@ -71,4 +72,13 @@ export const enableAllVerificationCodeSignInMethods = async (
signIn: {
methods: defaultVerificationCodeSignInMethods,
},
mfa: { factors: [], policy: MfaPolicy.UserControlled },
});
export const enableMandatoryMfaWithTotp = async () =>
updateSignInExperience({
mfa: {
factors: [MfaFactor.TOTP],
policy: MfaPolicy.Mandatory,
},
});

View file

@ -0,0 +1,150 @@
import { InteractionEvent, MfaFactor, SignInIdentifier } from '@logto/schemas';
import { authenticator } from 'otplib';
import { putInteraction, deleteUser, initTotp, putInteractionBindMfa } from '#src/api/index.js';
import { initClient, processSession, logoutClient } from '#src/helpers/client.js';
import { expectRejects } from '#src/helpers/index.js';
import {
enableAllPasswordSignInMethods,
enableMandatoryMfaWithTotp,
} from '#src/helpers/sign-in-experience.js';
import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js';
describe('register with mfa (mandatory TOTP)', () => {
beforeAll(async () => {
await enableAllPasswordSignInMethods({
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
});
await enableMandatoryMfaWithTotp();
});
it('should fail with missing_mfa error for normal register', async () => {
const { username, password } = generateNewUserProfile({ username: true, password: true });
const client = await initClient();
await client.send(putInteraction, {
event: InteractionEvent.Register,
profile: {
username,
password,
},
});
await expectRejects(client.submitInteraction(), {
code: 'user.missing_mfa',
statusCode: 422,
});
});
it('should fail with wrong totp code', async () => {
const { username, password } = generateNewUserProfile({ username: true, password: true });
const client = await initClient();
await client.send(putInteraction, {
event: InteractionEvent.Register,
profile: {
username,
password,
},
});
await client.send(initTotp);
await expectRejects(
client.send(putInteractionBindMfa, {
type: MfaFactor.TOTP,
code: '123456',
}),
{
code: 'session.mfa.invalid_totp_code',
statusCode: 400,
}
);
});
it('should register and setup TOTP successfully', async () => {
const { username, password } = generateNewUserProfile({ username: true, password: true });
const client = await initClient();
await client.send(putInteraction, {
event: InteractionEvent.Register,
profile: {
username,
password,
},
});
const { secret } = await client.send(initTotp);
const code = authenticator.generate(secret);
await client.send(putInteractionBindMfa, {
type: MfaFactor.TOTP,
code,
});
const { redirectTo } = await client.submitInteraction();
const id = await processSession(client, redirectTo);
await logoutClient(client);
await deleteUser(id);
});
});
describe('sign in and fulfill mfa (mandatory TOTP)', () => {
beforeAll(async () => {
await enableAllPasswordSignInMethods({
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
});
await enableMandatoryMfaWithTotp();
});
it('should fail with missing_mfa error for normal sign in', async () => {
const { userProfile, user } = await generateNewUser({ username: true, password: true });
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.SignIn,
identifier: {
username: userProfile.username,
password: userProfile.password,
},
});
await expectRejects(client.submitInteraction(), {
code: 'user.missing_mfa',
statusCode: 422,
});
await deleteUser(user.id);
});
it('should sign in and fulfill totp', async () => {
const { userProfile, user } = await generateNewUser({ username: true, password: true });
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.SignIn,
identifier: {
username: userProfile.username,
password: userProfile.password,
},
});
const { secret } = await client.send(initTotp);
const code = authenticator.generate(secret);
await client.send(putInteractionBindMfa, {
type: MfaFactor.TOTP,
code,
});
const { redirectTo } = await client.submitInteraction();
await processSession(client, redirectTo);
await logoutClient(client);
await deleteUser(user.id);
});
});

View file

@ -3679,6 +3679,10 @@ importers:
version: 3.20.2
packages/integration-tests:
dependencies:
otplib:
specifier: ^12.0.1
version: 12.0.1
devDependencies:
'@jest/test-sequencer':
specifier: ^29.5.0