diff --git a/packages/core/src/__mocks__/user.ts b/packages/core/src/__mocks__/user.ts index 35ec77dbc..f9883193e 100644 --- a/packages/core/src/__mocks__/user.ts +++ b/packages/core/src/__mocks__/user.ts @@ -15,6 +15,7 @@ export const mockUser: User = { identities: { connector1: { userId: 'connector1', details: {} }, }, + logtoConfig: {}, mfaVerifications: [], customData: {}, applicationId: 'bar', @@ -69,6 +70,7 @@ export const mockUserWithPassword: User = { connector1: { userId: 'connector1', details: {} }, }, customData: {}, + logtoConfig: {}, mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_789, @@ -89,6 +91,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + logtoConfig: {}, mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, @@ -107,6 +110,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + logtoConfig: {}, mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, @@ -125,6 +129,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + logtoConfig: {}, mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, @@ -143,6 +148,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + logtoConfig: {}, mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, @@ -161,6 +167,7 @@ export const mockUserList: User[] = [ avatar: null, identities: {}, customData: {}, + logtoConfig: {}, mfaVerifications: [], applicationId: 'bar', lastSignInAt: 1_650_969_465_000, diff --git a/packages/core/src/queries/user.test.ts b/packages/core/src/queries/user.test.ts index 6cd7daeef..81beaad58 100644 --- a/packages/core/src/queries/user.test.ts +++ b/packages/core/src/queries/user.test.ts @@ -39,6 +39,7 @@ describe('user query', () => { ...mockUser, identities: JSON.stringify(mockUser.identities), customData: JSON.stringify(mockUser.customData), + logtoConfig: JSON.stringify(mockUser.logtoConfig), mfaVerifications: JSON.stringify(mockUser.mfaVerifications), }; @@ -272,6 +273,7 @@ describe('user query', () => { ...mockUser, identities: JSON.stringify(restIdentities), customData: JSON.stringify(mockUser.customData), + logtoConfig: JSON.stringify(mockUser.logtoConfig), mfaVerifications: JSON.stringify(mockUser.mfaVerifications), }; diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index b704bb6ea..0dea2ab9a 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -183,7 +183,7 @@ describe('submit action', () => { { id: 'uid', ...upsertProfile, - customData: { + logtoConfig: { [userMfaDataKey]: { skipped: true, }, @@ -348,7 +348,7 @@ describe('submit action', () => { google: { userId: 'googleId', details: {} }, }, lastSignInAt: now, - customData: { + logtoConfig: { [userMfaDataKey]: { skipped: true, }, diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 3c72e46f6..6627293af 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -118,7 +118,7 @@ async function handleSubmitRegister( ), ...conditional( mfaSkipped && { - customData: { + logtoConfig: { [userMfaDataKey]: { skipped: true, }, @@ -174,8 +174,8 @@ async function handleSubmitSignIn( ), ...conditional( mfaSkipped && { - customData: { - ...user.customData, + logtoConfig: { + ...user.logtoConfig, [userMfaDataKey]: { skipped: true, }, diff --git a/packages/core/src/routes/interaction/verifications/mfa-verification.test.ts b/packages/core/src/routes/interaction/verifications/mfa-verification.test.ts index b54f2c9ea..e561293aa 100644 --- a/packages/core/src/routes/interaction/verifications/mfa-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/mfa-verification.test.ts @@ -200,7 +200,7 @@ describe('validateMandatoryBindMfa', () => { it('user mfaVerifications and bindMfa missing, mark skipped, and not required should pass', async () => { findUserById.mockResolvedValueOnce({ ...mockUser, - customData: { + logtoConfig: { mfa: { skipped: true }, }, }); diff --git a/packages/core/src/routes/interaction/verifications/mfa-verification.ts b/packages/core/src/routes/interaction/verifications/mfa-verification.ts index 67c4de7fb..8d08e5437 100644 --- a/packages/core/src/routes/interaction/verifications/mfa-verification.ts +++ b/packages/core/src/routes/interaction/verifications/mfa-verification.ts @@ -126,15 +126,29 @@ export const userMfaDataKey = 'mfa'; /** * Check if the user has skipped MFA binding */ -const isMfaSkipped = (customData: JsonObject): boolean => { +const isMfaSkipped = (logtoConfig: JsonObject): boolean => { const userMfaDataGuard = z.object({ skipped: z.boolean().optional(), }); - const parsed = z.object({ [userMfaDataKey]: userMfaDataGuard }).safeParse(customData); + const parsed = z.object({ [userMfaDataKey]: userMfaDataGuard }).safeParse(logtoConfig); return parsed.success ? parsed.data[userMfaDataKey].skipped === true : false; }; +/** + * Mark MFA as skipped in user custom data + */ +export const markMfaSkipped = async (tenant: TenantContext, accountId: string) => { + const { customData } = await tenant.queries.users.findUserById(accountId); + await tenant.queries.users.updateUserById(accountId, { + customData: { + ...customData, + [userMfaDataKey]: { + skipped: true, + }, + }, + }); +}; export const validateMandatoryBindMfa = async ( tenant: TenantContext, @@ -168,9 +182,9 @@ export const validateMandatoryBindMfa = async ( if (event === InteractionEvent.SignIn) { const { accountId } = interaction; - const { mfaVerifications, customData } = await tenant.queries.users.findUserById(accountId); + const { mfaVerifications, logtoConfig } = await tenant.queries.users.findUserById(accountId); - if (isMfaSkipped(customData)) { + if (isMfaSkipped(logtoConfig)) { return interaction; } diff --git a/packages/schemas/alterations/next-1698910485-user-logto-data.ts b/packages/schemas/alterations/next-1698910485-user-logto-data.ts new file mode 100644 index 000000000..29d5a98df --- /dev/null +++ b/packages/schemas/alterations/next-1698910485-user-logto-data.ts @@ -0,0 +1,20 @@ +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table users + add column if not exists logto_config jsonb not null default '{}'::jsonb; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table users + drop column logto_config; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/tables/users.sql b/packages/schemas/tables/users.sql index b72c95325..e873e6b71 100644 --- a/packages/schemas/tables/users.sql +++ b/packages/schemas/tables/users.sql @@ -16,6 +16,7 @@ create table users ( application_id varchar(21), identities jsonb /* @use Identities */ not null default '{}'::jsonb, custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb, + logto_config jsonb /* @use JsonObject */ not null default '{}'::jsonb, mfa_verifications jsonb /* @use MfaVerifications */ not null default '[]'::jsonb, is_suspended boolean not null default false, last_sign_in_at timestamptz,