0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

fix(core): fix validateBindMfaBackupCode to force linking backup code (#4753)

This commit is contained in:
wangsijie 2023-10-27 14:55:48 +08:00 committed by GitHub
parent d221b30b35
commit 97d1dfaa90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 89 additions and 10 deletions

View file

@ -7,7 +7,11 @@ import type Provider from 'oidc-provider';
import { mockBackupCodeBind, mockTotpBind } from '#src/__mocks__/mfa-verification.js';
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
import { mockUser, mockUserWithMfaVerifications } from '#src/__mocks__/user.js';
import {
mockUser,
mockUserBackupCodeMfaVerification,
mockUserWithMfaVerifications,
} from '#src/__mocks__/user.js';
import RequestError from '#src/errors/RequestError/index.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { MockTenant } from '#src/test-utils/tenant.js';
@ -291,12 +295,6 @@ describe('verifyMfa', () => {
});
describe('validateBindMfaBackupCode', () => {
it('should pass if bindMfas is empty', async () => {
await expect(
validateBindMfaBackupCode(tenantContext, baseCtx, signInInteraction, provider)
).resolves.not.toThrow();
});
it('should pass if backup code is not enabled', async () => {
await expect(
validateBindMfaBackupCode(
@ -311,7 +309,7 @@ describe('validateBindMfaBackupCode', () => {
).resolves.not.toThrow();
});
it('should pass if backup code is set', async () => {
it('should pass if backup code is set in bindMfas', async () => {
await expect(
validateBindMfaBackupCode(
tenantContext,
@ -325,6 +323,72 @@ describe('validateBindMfaBackupCode', () => {
).resolves.not.toThrow();
});
it('should pass if there is no other MFA for register', async () => {
await expect(
validateBindMfaBackupCode(tenantContext, backupCodeEnabledCtx, interaction, provider)
).resolves.not.toThrow();
});
it('should pass if there is no other MFA for sign in', async () => {
findUserById.mockResolvedValueOnce(mockUser);
await expect(
validateBindMfaBackupCode(
tenantContext,
backupCodeEnabledCtx,
{
...signInInteraction,
},
provider
)
).resolves.not.toThrow();
});
it('should pass if backup code is set in user mfaVerifications', async () => {
findUserById.mockResolvedValueOnce({
...mockUser,
mfaVerifications: [mockUserBackupCodeMfaVerification],
});
await expect(
validateBindMfaBackupCode(
tenantContext,
backupCodeEnabledCtx,
{
...signInInteraction,
bindMfas: [mockTotpBind],
},
provider
)
).resolves.not.toThrow();
});
it('should reject if backup code is set in user mfaVerifications but used', async () => {
findUserById.mockResolvedValueOnce({
...mockUser,
mfaVerifications: [
{
...mockUserBackupCodeMfaVerification,
codes: [{ code: 'code', usedAt: new Date().toISOString() }],
},
],
});
await expect(
validateBindMfaBackupCode(
tenantContext,
backupCodeEnabledCtx,
{
...signInInteraction,
bindMfas: [mockTotpBind],
},
provider
)
).rejects.toThrowError(
new RequestError(
{ code: 'session.mfa.backup_code_required', status: 422 },
{ codes: mockBackupCodes }
)
);
});
it('should reject if backup code is not set', async () => {
findUserById.mockResolvedValueOnce(mockUserWithMfaVerifications);
@ -334,7 +398,7 @@ describe('validateBindMfaBackupCode', () => {
backupCodeEnabledCtx,
{
...signInInteraction,
bindMfas: [mockTotpBind],
bindMfas: [],
},
provider
)

View file

@ -222,16 +222,31 @@ export const validateBindMfaBackupCode = async (
if (
!factors.includes(MfaFactor.BackupCode) ||
bindMfas.length === 0 ||
bindMfas.some(({ type }) => type === MfaFactor.BackupCode)
) {
return interaction;
}
// Skip check if there is no other MFA
if (
event === InteractionEvent.Register &&
!bindMfas.some(({ type }) => type !== MfaFactor.BackupCode)
) {
return interaction;
}
if (event === InteractionEvent.SignIn) {
const { accountId } = interaction;
const { mfaVerifications } = await tenant.queries.users.findUserById(accountId);
// Skip check if there is no new MFA and there is no existing MFA configured
if (
!bindMfas.some(({ type }) => type !== MfaFactor.BackupCode) &&
!mfaVerifications.some(({ type }) => type !== MfaFactor.BackupCode)
) {
return interaction;
}
if (
mfaVerifications.some((verification) => {
return (