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:
parent
a8e8703898
commit
c3f865ac2c
5 changed files with 188 additions and 2 deletions
|
@ -53,5 +53,8 @@
|
|||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc"
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"dependencies": {
|
||||
"otplib": "^12.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue