diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index 5ca4476a2..e4dc5f3b0 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -6,7 +6,7 @@ import type { SignUp, SignIn, } from '@logto/schemas'; -import { SignInMode, SignInIdentifier } from '@logto/schemas'; +import { SignInMode, SignInIdentifier, MfaPolicy } from '@logto/schemas'; export const mockColor: Color = { primaryColor: '#000', @@ -92,4 +92,8 @@ export const mockSignInExperience: SignInExperience = { customCss: null, customContent: {}, passwordPolicy: {}, + mfa: { + policy: MfaPolicy.UserControlled, + factors: [], + }, }; diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index c0502812e..850067c53 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -33,12 +33,13 @@ describe('sign-in-experience query', () => { socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets), customContent: JSON.stringify(mockSignInExperience.customContent), passwordPolicy: JSON.stringify(mockSignInExperience.passwordPolicy), + mfa: JSON.stringify(mockSignInExperience.mfa), }; it('findDefaultSignInExperience', async () => { /* eslint-disable sql/no-unsafe-query */ const expectSql = ` - select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css", "custom_content", "password_policy" + select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css", "custom_content", "password_policy", "mfa" from "sign_in_experiences" where "id"=$1 `; diff --git a/packages/schemas/alterations/next-1694403476-sie-mfa.ts b/packages/schemas/alterations/next-1694403476-sie-mfa.ts new file mode 100644 index 000000000..50d101bb2 --- /dev/null +++ b/packages/schemas/alterations/next-1694403476-sie-mfa.ts @@ -0,0 +1,26 @@ +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table sign_in_experiences + add column if not exists mfa jsonb not null default '{}'::jsonb; + `); + + await pool.query(sql` + update sign_in_experiences + set mfa = '{"factors":[],"policy":"UserControlled"}' + where id = 'default'; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table sign_in_experiences + drop column mfa; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 24dc688df..83033b9e1 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -164,6 +164,28 @@ export const customContentGuard = z.record(z.string()); export type CustomContent = z.infer; +export enum MfaFactor { + TOTP = 'TOTP', + WebAuthn = 'WebAuthn', + BackupCode = 'BackupCode', +} + +export const mfaFactorsGuard = z.nativeEnum(MfaFactor).array(); + +export type MfaFactors = z.infer; + +export enum MfaPolicy { + UserControlled = 'UserControlled', + Mandatory = 'Mandatory', +} + +export const mfaGuard = z.object({ + factors: mfaFactorsGuard, + policy: z.nativeEnum(MfaPolicy), +}); + +export type Mfa = z.infer; + /* === Phrases === */ export type Translation = { diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index b6c2ae92c..e875ff29f 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -2,7 +2,7 @@ import { generateDarkColor } from '@logto/core-kit'; import type { CreateSignInExperience } from '../db-entries/index.js'; import { SignInMode } from '../db-entries/index.js'; -import { SignInIdentifier } from '../foundations/index.js'; +import { MfaPolicy, SignInIdentifier } from '../foundations/index.js'; import { adminTenantId, defaultTenantId } from './tenant.js'; @@ -50,6 +50,10 @@ export const createDefaultSignInExperience = ( customCss: null, customContent: {}, passwordPolicy: {}, + mfa: { + factors: [], + policy: MfaPolicy.UserControlled, + }, }); /** @deprecated Use `createDefaultSignInExperience()` instead. */ diff --git a/packages/schemas/tables/sign_in_experiences.sql b/packages/schemas/tables/sign_in_experiences.sql index 200453282..5f036ac4c 100644 --- a/packages/schemas/tables/sign_in_experiences.sql +++ b/packages/schemas/tables/sign_in_experiences.sql @@ -16,5 +16,6 @@ create table sign_in_experiences ( custom_css text, custom_content jsonb /* @use CustomContent */ not null default '{}'::jsonb, password_policy jsonb /* @use PartialPasswordPolicy */ not null default '{}'::jsonb, + mfa jsonb /* @use Mfa */ not null default '{}'::jsonb, primary key (tenant_id, id) ); diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index 79f124442..0f280b188 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -1,134 +1,21 @@ import type { SignInExperience, SignIn } from '@logto/schemas'; -import { ConnectorPlatform, ConnectorType, SignInIdentifier, SignInMode } from '@logto/schemas'; +import { + ConnectorPlatform, + ConnectorType, + MfaPolicy, + SignInIdentifier, + SignInMode, +} from '@logto/schemas'; import type { SignInExperienceResponse } from '@/types'; +import { socialConnectors } from './social-connectors'; + +export * from './social-connectors'; + export const appLogo = 'https://avatars.githubusercontent.com/u/88327661?s=200&v=4'; export const appHeadline = 'Build user identity in a modern way'; -export const socialConnectors = [ - { - id: 'BE8QXN0VsrOH7xdWFDJZ9', - target: 'github', - platform: ConnectorPlatform.Web, - type: ConnectorType.Social, - logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - logoDark: null, - name: { - en: 'Sign in with GitHub', - 'pt-PT': 'Entrar com GitHub', - 'zh-CN': '使用 GitHub 登录', - 'tr-TR': 'Github ile giriş yap', - ko: 'Github 로그인', - }, - description: { - en: 'Sign in with GitHub', - 'pt-PT': 'Entrar com GitHub', - 'zh-CN': '使用 GitHub 登录', - 'tr-TR': 'Github ile giriş yap', - ko: 'Github 로그인', - }, - readme: '', - configTemplate: '', - }, - { - id: '24yt_xIUl5btN4UwvFokt', - target: 'alipay', - platform: ConnectorPlatform.Web, - type: ConnectorType.Social, - logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - logoDark: null, - name: { - en: 'Sign in with Alipay', - 'pt-PT': 'Entrar com Alipay', - 'zh-CN': '使用 Alipay 登录', - 'tr-TR': 'Alipay ile giriş yap', - ko: 'Alipay 로그인', - }, - description: { - en: 'Sign in with Alipay', - 'pt-PT': 'Entrar com Alipay', - 'zh-CN': '使用 Alipay 登录', - 'tr-TR': 'Alipay ile giriş yap', - ko: 'Alipay 로그인', - }, - readme: '', - configTemplate: '', - }, - { - id: 'E5kb2gdq769qOEYaLg1V5', - target: 'wechat', - platform: ConnectorPlatform.Web, - type: ConnectorType.Social, - logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - logoDark: null, - name: { - en: 'Sign in with WeChat', - 'pt-PT': 'Entrar com WeChat', - 'zh-CN': '使用 WeChat 登录', - 'tr-TR': 'WeChat ile giriş yap', - ko: 'WeChat 로그인', - }, - description: { - en: 'Sign in with WeChat', - 'pt-PT': 'Entrar com WeChat', - 'zh-CN': '使用 WeChat 登录', - 'tr-TR': 'WeChat ile giriş yap', - ko: 'WeChat 로그인', - }, - readme: '', - configTemplate: '', - }, - { - id: 'xY2YZEweMFPKxphngGHhy', - target: 'google', - platform: ConnectorPlatform.Web, - type: ConnectorType.Social, - logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - logoDark: null, - name: { - en: 'Sign in with Google', - 'pt-PT': 'Entrar com Google', - 'zh-CN': '使用 Google 登录', - 'tr-TR': 'Google ile giriş yap', - ko: 'Google 로그인', - }, - description: { - en: 'Sign in with Google', - 'pt-PT': 'Entrar com Google', - 'zh-CN': '使用 Google 登录', - 'tr-TR': 'Google ile giriş yap', - ko: 'Google 로그인', - }, - readme: '', - configTemplate: '', - }, - { - id: 'lcXT4o2GSjbV9kg2shZC7', - target: 'facebook', - platform: ConnectorPlatform.Web, - type: ConnectorType.Social, - logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - logoDark: null, - name: { - en: 'Sign in with Meta', - 'pt-PT': 'Entrar com Facebook', - 'zh-CN': '使用 Meta 登录', - 'tr-TR': 'Meta ile giriş yap', - ko: 'Meta 로그인', - }, - description: { - en: 'Sign in with Meta', - 'pt-PT': 'Entrar com Facebook', - 'zh-CN': '使用 Meta 登录', - 'tr-TR': 'Meta ile giriş yap', - ko: 'Meta 로그인', - }, - readme: '', - configTemplate: '', - }, -]; - export const mockSocialConnectorData = { id: 'arbitrary-social-connector-data', target: 'google', @@ -205,6 +92,10 @@ export const mockSignInExperience: SignInExperience = { customCss: null, customContent: {}, passwordPolicy: {}, + mfa: { + policy: MfaPolicy.UserControlled, + factors: [], + }, }; export const mockSignInExperienceSettings: SignInExperienceResponse = { @@ -230,6 +121,10 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = { customCss: null, customContent: {}, passwordPolicy: {}, + mfa: { + policy: MfaPolicy.UserControlled, + factors: [], + }, }; const usernameSettings = { diff --git a/packages/ui/src/__mocks__/social-connectors.tsx b/packages/ui/src/__mocks__/social-connectors.tsx new file mode 100644 index 000000000..9f87eb583 --- /dev/null +++ b/packages/ui/src/__mocks__/social-connectors.tsx @@ -0,0 +1,124 @@ +import { ConnectorPlatform, ConnectorType } from '@logto/connector-kit'; + +export const socialConnectors = [ + { + id: 'BE8QXN0VsrOH7xdWFDJZ9', + target: 'github', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, + logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', + logoDark: null, + name: { + en: 'Sign in with GitHub', + 'pt-PT': 'Entrar com GitHub', + 'zh-CN': '使用 GitHub 登录', + 'tr-TR': 'Github ile giriş yap', + ko: 'Github 로그인', + }, + description: { + en: 'Sign in with GitHub', + 'pt-PT': 'Entrar com GitHub', + 'zh-CN': '使用 GitHub 登录', + 'tr-TR': 'Github ile giriş yap', + ko: 'Github 로그인', + }, + readme: '', + configTemplate: '', + }, + { + id: '24yt_xIUl5btN4UwvFokt', + target: 'alipay', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, + logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', + logoDark: null, + name: { + en: 'Sign in with Alipay', + 'pt-PT': 'Entrar com Alipay', + 'zh-CN': '使用 Alipay 登录', + 'tr-TR': 'Alipay ile giriş yap', + ko: 'Alipay 로그인', + }, + description: { + en: 'Sign in with Alipay', + 'pt-PT': 'Entrar com Alipay', + 'zh-CN': '使用 Alipay 登录', + 'tr-TR': 'Alipay ile giriş yap', + ko: 'Alipay 로그인', + }, + readme: '', + configTemplate: '', + }, + { + id: 'E5kb2gdq769qOEYaLg1V5', + target: 'wechat', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, + logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', + logoDark: null, + name: { + en: 'Sign in with WeChat', + 'pt-PT': 'Entrar com WeChat', + 'zh-CN': '使用 WeChat 登录', + 'tr-TR': 'WeChat ile giriş yap', + ko: 'WeChat 로그인', + }, + description: { + en: 'Sign in with WeChat', + 'pt-PT': 'Entrar com WeChat', + 'zh-CN': '使用 WeChat 登录', + 'tr-TR': 'WeChat ile giriş yap', + ko: 'WeChat 로그인', + }, + readme: '', + configTemplate: '', + }, + { + id: 'xY2YZEweMFPKxphngGHhy', + target: 'google', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, + logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', + logoDark: null, + name: { + en: 'Sign in with Google', + 'pt-PT': 'Entrar com Google', + 'zh-CN': '使用 Google 登录', + 'tr-TR': 'Google ile giriş yap', + ko: 'Google 로그인', + }, + description: { + en: 'Sign in with Google', + 'pt-PT': 'Entrar com Google', + 'zh-CN': '使用 Google 登录', + 'tr-TR': 'Google ile giriş yap', + ko: 'Google 로그인', + }, + readme: '', + configTemplate: '', + }, + { + id: 'lcXT4o2GSjbV9kg2shZC7', + target: 'facebook', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, + logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', + logoDark: null, + name: { + en: 'Sign in with Meta', + 'pt-PT': 'Entrar com Facebook', + 'zh-CN': '使用 Meta 登录', + 'tr-TR': 'Meta ile giriş yap', + ko: 'Meta 로그인', + }, + description: { + en: 'Sign in with Meta', + 'pt-PT': 'Entrar com Facebook', + 'zh-CN': '使用 Meta 登录', + 'tr-TR': 'Meta ile giriş yap', + ko: 'Meta 로그인', + }, + readme: '', + configTemplate: '', + }, +];