0
Fork 0
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:
wangsijie 2023-09-12 17:53:16 +08:00 committed by GitHub
parent 65f83aeb58
commit 6cdd33bf1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 145 additions and 2 deletions

View 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'));
});
});

View 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');
}
};

View file

@ -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(

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);