mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
feat(core): update mfa in sign in experience (#4467)
This commit is contained in:
parent
65f83aeb58
commit
6cdd33bf1c
19 changed files with 145 additions and 2 deletions
54
packages/core/src/libraries/sign-in-experience/mfa.test.ts
Normal file
54
packages/core/src/libraries/sign-in-experience/mfa.test.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { MfaFactor, MfaPolicy } from '@logto/schemas';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import { validateMfa } from './mfa.js';
|
||||
|
||||
describe('validate mfa', () => {
|
||||
describe('pass on valid cases', () => {
|
||||
it('should pass on empty factors', () => {
|
||||
expect(() => {
|
||||
validateMfa({
|
||||
factors: [],
|
||||
policy: MfaPolicy.UserControlled,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass on TOTP only', () => {
|
||||
expect(() => {
|
||||
validateMfa({
|
||||
factors: [MfaFactor.TOTP],
|
||||
policy: MfaPolicy.UserControlled,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass on TOTP with backup code', () => {
|
||||
expect(() => {
|
||||
validateMfa({
|
||||
factors: [MfaFactor.TOTP, MfaFactor.BackupCode],
|
||||
policy: MfaPolicy.UserControlled,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on backup code alone', () => {
|
||||
expect(() => {
|
||||
validateMfa({
|
||||
factors: [MfaFactor.BackupCode],
|
||||
policy: MfaPolicy.UserControlled,
|
||||
});
|
||||
}).toMatchError(new RequestError('sign_in_experiences.backup_code_cannot_be_enabled_alone'));
|
||||
});
|
||||
|
||||
it('should throw on duplicated factors', () => {
|
||||
expect(() => {
|
||||
validateMfa({
|
||||
factors: [MfaFactor.TOTP, MfaFactor.TOTP],
|
||||
policy: MfaPolicy.UserControlled,
|
||||
});
|
||||
}).toMatchError(new RequestError('sign_in_experiences.duplicated_mfa_factors'));
|
||||
});
|
||||
});
|
22
packages/core/src/libraries/sign-in-experience/mfa.ts
Normal file
22
packages/core/src/libraries/sign-in-experience/mfa.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { MfaFactor, type Mfa } from '@logto/schemas';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
export const validateMfa = (mfa: Mfa) => {
|
||||
// TODO @sijie: remove this check when MFA is ready for production.
|
||||
if (EnvSet.values.isProduction && !EnvSet.values.isIntegrationTest) {
|
||||
throw new Error('MFA is not ready for production yet.');
|
||||
}
|
||||
|
||||
assertThat(
|
||||
new Set(mfa.factors).size === mfa.factors.length,
|
||||
'sign_in_experiences.duplicated_mfa_factors'
|
||||
);
|
||||
|
||||
const backupCodeEnabled = mfa.factors.includes(MfaFactor.BackupCode);
|
||||
|
||||
if (backupCodeEnabled) {
|
||||
assertThat(mfa.factors.length > 1, 'sign_in_experiences.backup_code_cannot_be_enabled_alone');
|
||||
}
|
||||
};
|
|
@ -3,6 +3,7 @@ import { ConnectorType, SignInExperiences } from '@logto/schemas';
|
|||
import { literal, object, string, z } from 'zod';
|
||||
|
||||
import { validateSignUp, validateSignIn } from '#src/libraries/sign-in-experience/index.js';
|
||||
import { validateMfa } from '#src/libraries/sign-in-experience/mfa.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
@ -55,7 +56,7 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(
|
|||
query: { removeUnusedDemoSocialConnector },
|
||||
body: { socialSignInConnectorTargets, ...rest },
|
||||
} = ctx.guard;
|
||||
const { languageInfo, signUp, signIn } = rest;
|
||||
const { languageInfo, signUp, signIn, mfa } = rest;
|
||||
|
||||
if (languageInfo) {
|
||||
await validateLanguageInfo(languageInfo);
|
||||
|
@ -82,6 +83,10 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(
|
|||
validateSignIn(signIn, signInExperience.signUp, connectors);
|
||||
}
|
||||
|
||||
if (mfa) {
|
||||
validateMfa(mfa);
|
||||
}
|
||||
|
||||
// Remove unused demo social connectors, those that are not selected in onboarding SIE config.
|
||||
if (removeUnusedDemoSocialConnector && filteredSocialSignInConnectorTargets) {
|
||||
await Promise.all(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { MfaPolicy, SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import { getSignInExperience, updateSignInExperience } from '#src/api/index.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
|
@ -23,6 +23,10 @@ describe('admin console sign-in experience', () => {
|
|||
},
|
||||
termsOfUseUrl: 'https://logto.io/terms',
|
||||
privacyPolicyUrl: 'https://logto.io/privacy',
|
||||
mfa: {
|
||||
policy: MfaPolicy.UserControlled,
|
||||
factors: [],
|
||||
},
|
||||
};
|
||||
|
||||
const updatedSignInExperience = await updateSignInExperience(newSignInExperience);
|
||||
|
|
|
@ -19,6 +19,10 @@ const sign_in_experiences = {
|
|||
unsupported_default_language: 'Die Sprache - {{language}} wird momentan nicht unterstützt.',
|
||||
at_least_one_authentication_factor:
|
||||
'Sie müssen mindestens einen Authentifizierungsfaktor auswählen.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -15,6 +15,8 @@ const sign_in_experiences = {
|
|||
'Verification code sign in must be enabled when set a password is not required in sign up.',
|
||||
unsupported_default_language: 'This language - {{language}} is not supported at the moment.',
|
||||
at_least_one_authentication_factor: 'You have to select at least one authentication factor.',
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -18,6 +18,10 @@ const sign_in_experiences = {
|
|||
'La firma de código de verificación debe estar habilitada cuando no se requiere contraseña en el registro.',
|
||||
unsupported_default_language: 'Este lenguaje - {{language}} no es compatible en este momento.',
|
||||
at_least_one_authentication_factor: 'Debe seleccionar al menos un factor de autenticación.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -20,6 +20,10 @@ const sign_in_experiences = {
|
|||
"Cette langue - {{language}} n'est pas prise en charge pour le moment.",
|
||||
at_least_one_authentication_factor:
|
||||
"Vous devez sélectionner au moins un facteur d'authentification.",
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -18,6 +18,10 @@ const sign_in_experiences = {
|
|||
'Il metodo di accesso con codice di verifica deve essere abilitato quando non è richiesta una password nella registrazione.',
|
||||
unsupported_default_language: 'Questa lingua - {{language}} non è supportata al momento.',
|
||||
at_least_one_authentication_factor: 'Devi selezionare almeno un fattore di autenticazione.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -18,6 +18,10 @@ const sign_in_experiences = {
|
|||
'サインアップ時にパスワードが不要な場合、検証コードサインインを有効にする必要があります。',
|
||||
unsupported_default_language: 'この言語- {{language}} は、現時点ではサポートされていません。',
|
||||
at_least_one_authentication_factor: '認証ファクタを1つ以上選択する必要があります。',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -15,6 +15,10 @@ const sign_in_experiences = {
|
|||
'비밀번호를 설정할 필요가 없을 때는 인증 코드 로그인을 활성화해야 해요.',
|
||||
unsupported_default_language: '{{language}} 언어는 아직 지원하지 않아요.',
|
||||
at_least_one_authentication_factor: '최소한 하나의 인증 방법을 선택해야 해요.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -17,6 +17,10 @@ const sign_in_experiences = {
|
|||
'Logowanie za pomocą kodu weryfikacyjnego musi być włączone, gdy w rejestracji nie jest wymagane ustawienie hasła.',
|
||||
unsupported_default_language: 'Ten język - {{language}} nie jest obecnie obsługiwany.',
|
||||
at_least_one_authentication_factor: 'Musisz wybrać co najmniej jeden czynnik uwierzytelniający.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -18,6 +18,10 @@ const sign_in_experiences = {
|
|||
'O login do código de verificação deve ser ativado quando definir uma senha não é necessária na inscrição.',
|
||||
unsupported_default_language: 'Este idioma - {{language}} não é suportado no momento.',
|
||||
at_least_one_authentication_factor: 'Você deve selecionar pelo menos um fator de autenticação.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -18,6 +18,10 @@ const sign_in_experiences = {
|
|||
'O login com código de verificação deve ser habilitado quando não é requerido configurar uma senha na inscrição.',
|
||||
unsupported_default_language: 'Este idioma - {{language}} não é suportado no momento.',
|
||||
at_least_one_authentication_factor: 'Você deve selecionar pelo menos um fator de autenticação.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -18,6 +18,10 @@ const sign_in_experiences = {
|
|||
'Вход в систему по коду проверки должен быть включен, когда для создания учетной записи не требуется установка пароля.',
|
||||
unsupported_default_language: 'Этот язык - {{language}} не поддерживается в данный момент.',
|
||||
at_least_one_authentication_factor: 'Вы должны выбрать как минимум один фактор аутентификации.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -16,6 +16,10 @@ const sign_in_experiences = {
|
|||
'Kayıtta şifre belirleme zorunlu olmadığında doğrulama koduyla oturum açma etkinleştirilmelidir.',
|
||||
unsupported_default_language: 'Bu dil - {{language}}, şu anda desteklenmemektedir.',
|
||||
at_least_one_authentication_factor: 'En az bir doğrulama faktörü seçmelisiniz.',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -10,6 +10,10 @@ const sign_in_experiences = {
|
|||
code_sign_in_must_be_enabled: '必须在注册中不要求设置密码时启用验证码登录。',
|
||||
unsupported_default_language: '{{language}} 无法选择为默认语言。',
|
||||
at_least_one_authentication_factor: '至少要选择一个登录要素',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -11,6 +11,10 @@ const sign_in_experiences = {
|
|||
code_sign_in_must_be_enabled: '必須喺註冊中唔要求設置密碼時啟用驗證碼登錄。',
|
||||
unsupported_default_language: '{{language}} 無法選擇做默認語言。',
|
||||
at_least_one_authentication_factor: '至少要揀一個登錄要素',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
|
@ -10,6 +10,10 @@ const sign_in_experiences = {
|
|||
code_sign_in_must_be_enabled: '必須在註冊中不要求設置密碼時啟用驗證碼登錄。',
|
||||
unsupported_default_language: '{{language}} 無法選擇為默認語言。',
|
||||
at_least_one_authentication_factor: '至少要選擇一個登錄要素',
|
||||
/** UNTRANSLATED */
|
||||
backup_code_cannot_be_enabled_alone: 'Backup code cannot be enabled alone.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_mfa_factors: 'Duplicated MFA factors.',
|
||||
};
|
||||
|
||||
export default Object.freeze(sign_in_experiences);
|
||||
|
|
Loading…
Add table
Reference in a new issue