mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -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": {
|
"eslintConfig": {
|
||||||
"extends": "@silverhand"
|
"extends": "@silverhand"
|
||||||
},
|
},
|
||||||
"prettier": "@silverhand/eslint-config/.prettierrc"
|
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||||
|
"dependencies": {
|
||||||
|
"otplib": "^12.0.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type {
|
||||||
IdentifierPayload,
|
IdentifierPayload,
|
||||||
Profile,
|
Profile,
|
||||||
RequestVerificationCodePayload,
|
RequestVerificationCodePayload,
|
||||||
|
BindMfaPayload,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import type { Got } from 'got';
|
import type { Got } from 'got';
|
||||||
|
|
||||||
|
@ -67,6 +68,15 @@ export const putInteractionProfile = async (cookie: string, payload: Profile) =>
|
||||||
})
|
})
|
||||||
.json();
|
.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) =>
|
export const deleteInteractionProfile = async (cookie: string) =>
|
||||||
api
|
api
|
||||||
.delete('interaction/profile', {
|
.delete('interaction/profile', {
|
||||||
|
@ -106,6 +116,15 @@ export const createSocialAuthorizationUri = async (
|
||||||
followRedirect: false,
|
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) =>
|
export const consent = async (api: Got, cookie: string) =>
|
||||||
api
|
api
|
||||||
.post('interaction/consent', {
|
.post('interaction/consent', {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { SignInExperience } from '@logto/schemas';
|
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';
|
import { updateSignInExperience } from '#src/api/index.js';
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ export const enableAllPasswordSignInMethods = async (
|
||||||
signIn: {
|
signIn: {
|
||||||
methods: defaultPasswordSignInMethods,
|
methods: defaultPasswordSignInMethods,
|
||||||
},
|
},
|
||||||
|
mfa: { factors: [], policy: MfaPolicy.UserControlled },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const enableAllVerificationCodeSignInMethods = async (
|
export const enableAllVerificationCodeSignInMethods = async (
|
||||||
|
@ -71,4 +72,13 @@ export const enableAllVerificationCodeSignInMethods = async (
|
||||||
signIn: {
|
signIn: {
|
||||||
methods: defaultVerificationCodeSignInMethods,
|
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
|
version: 3.20.2
|
||||||
|
|
||||||
packages/integration-tests:
|
packages/integration-tests:
|
||||||
|
dependencies:
|
||||||
|
otplib:
|
||||||
|
specifier: ^12.0.1
|
||||||
|
version: 12.0.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@jest/test-sequencer':
|
'@jest/test-sequencer':
|
||||||
specifier: ^29.5.0
|
specifier: ^29.5.0
|
||||||
|
|
Loading…
Reference in a new issue