0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00

refactor(core,schemas): add user logto_data column to store mfa (#4792)

* feat(core,phrases): disable auto skip mfa

* refactor(experience): skip mfa manually (#4788)

* refactor(core,schemas): add user logto_data column to store mfa skipped info

---------

Co-authored-by: Xiao Yijun <xiaoyijun@silverhand.io>
This commit is contained in:
wangsijie 2023-11-02 16:16:21 +08:00 committed by GitHub
parent 9ea79a18d6
commit b118fc54a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 54 additions and 10 deletions

View file

@ -15,6 +15,7 @@ export const mockUser: User = {
identities: { identities: {
connector1: { userId: 'connector1', details: {} }, connector1: { userId: 'connector1', details: {} },
}, },
logtoConfig: {},
mfaVerifications: [], mfaVerifications: [],
customData: {}, customData: {},
applicationId: 'bar', applicationId: 'bar',
@ -69,6 +70,7 @@ export const mockUserWithPassword: User = {
connector1: { userId: 'connector1', details: {} }, connector1: { userId: 'connector1', details: {} },
}, },
customData: {}, customData: {},
logtoConfig: {},
mfaVerifications: [], mfaVerifications: [],
applicationId: 'bar', applicationId: 'bar',
lastSignInAt: 1_650_969_465_789, lastSignInAt: 1_650_969_465_789,
@ -89,6 +91,7 @@ export const mockUserList: User[] = [
avatar: null, avatar: null,
identities: {}, identities: {},
customData: {}, customData: {},
logtoConfig: {},
mfaVerifications: [], mfaVerifications: [],
applicationId: 'bar', applicationId: 'bar',
lastSignInAt: 1_650_969_465_000, lastSignInAt: 1_650_969_465_000,
@ -107,6 +110,7 @@ export const mockUserList: User[] = [
avatar: null, avatar: null,
identities: {}, identities: {},
customData: {}, customData: {},
logtoConfig: {},
mfaVerifications: [], mfaVerifications: [],
applicationId: 'bar', applicationId: 'bar',
lastSignInAt: 1_650_969_465_000, lastSignInAt: 1_650_969_465_000,
@ -125,6 +129,7 @@ export const mockUserList: User[] = [
avatar: null, avatar: null,
identities: {}, identities: {},
customData: {}, customData: {},
logtoConfig: {},
mfaVerifications: [], mfaVerifications: [],
applicationId: 'bar', applicationId: 'bar',
lastSignInAt: 1_650_969_465_000, lastSignInAt: 1_650_969_465_000,
@ -143,6 +148,7 @@ export const mockUserList: User[] = [
avatar: null, avatar: null,
identities: {}, identities: {},
customData: {}, customData: {},
logtoConfig: {},
mfaVerifications: [], mfaVerifications: [],
applicationId: 'bar', applicationId: 'bar',
lastSignInAt: 1_650_969_465_000, lastSignInAt: 1_650_969_465_000,
@ -161,6 +167,7 @@ export const mockUserList: User[] = [
avatar: null, avatar: null,
identities: {}, identities: {},
customData: {}, customData: {},
logtoConfig: {},
mfaVerifications: [], mfaVerifications: [],
applicationId: 'bar', applicationId: 'bar',
lastSignInAt: 1_650_969_465_000, lastSignInAt: 1_650_969_465_000,

View file

@ -39,6 +39,7 @@ describe('user query', () => {
...mockUser, ...mockUser,
identities: JSON.stringify(mockUser.identities), identities: JSON.stringify(mockUser.identities),
customData: JSON.stringify(mockUser.customData), customData: JSON.stringify(mockUser.customData),
logtoConfig: JSON.stringify(mockUser.logtoConfig),
mfaVerifications: JSON.stringify(mockUser.mfaVerifications), mfaVerifications: JSON.stringify(mockUser.mfaVerifications),
}; };
@ -272,6 +273,7 @@ describe('user query', () => {
...mockUser, ...mockUser,
identities: JSON.stringify(restIdentities), identities: JSON.stringify(restIdentities),
customData: JSON.stringify(mockUser.customData), customData: JSON.stringify(mockUser.customData),
logtoConfig: JSON.stringify(mockUser.logtoConfig),
mfaVerifications: JSON.stringify(mockUser.mfaVerifications), mfaVerifications: JSON.stringify(mockUser.mfaVerifications),
}; };

View file

@ -183,7 +183,7 @@ describe('submit action', () => {
{ {
id: 'uid', id: 'uid',
...upsertProfile, ...upsertProfile,
customData: { logtoConfig: {
[userMfaDataKey]: { [userMfaDataKey]: {
skipped: true, skipped: true,
}, },
@ -348,7 +348,7 @@ describe('submit action', () => {
google: { userId: 'googleId', details: {} }, google: { userId: 'googleId', details: {} },
}, },
lastSignInAt: now, lastSignInAt: now,
customData: { logtoConfig: {
[userMfaDataKey]: { [userMfaDataKey]: {
skipped: true, skipped: true,
}, },

View file

@ -118,7 +118,7 @@ async function handleSubmitRegister(
), ),
...conditional( ...conditional(
mfaSkipped && { mfaSkipped && {
customData: { logtoConfig: {
[userMfaDataKey]: { [userMfaDataKey]: {
skipped: true, skipped: true,
}, },
@ -174,8 +174,8 @@ async function handleSubmitSignIn(
), ),
...conditional( ...conditional(
mfaSkipped && { mfaSkipped && {
customData: { logtoConfig: {
...user.customData, ...user.logtoConfig,
[userMfaDataKey]: { [userMfaDataKey]: {
skipped: true, skipped: true,
}, },

View file

@ -200,7 +200,7 @@ describe('validateMandatoryBindMfa', () => {
it('user mfaVerifications and bindMfa missing, mark skipped, and not required should pass', async () => { it('user mfaVerifications and bindMfa missing, mark skipped, and not required should pass', async () => {
findUserById.mockResolvedValueOnce({ findUserById.mockResolvedValueOnce({
...mockUser, ...mockUser,
customData: { logtoConfig: {
mfa: { skipped: true }, mfa: { skipped: true },
}, },
}); });

View file

@ -126,15 +126,29 @@ export const userMfaDataKey = 'mfa';
/** /**
* Check if the user has skipped MFA binding * Check if the user has skipped MFA binding
*/ */
const isMfaSkipped = (customData: JsonObject): boolean => { const isMfaSkipped = (logtoConfig: JsonObject): boolean => {
const userMfaDataGuard = z.object({ const userMfaDataGuard = z.object({
skipped: z.boolean().optional(), 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; 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 ( export const validateMandatoryBindMfa = async (
tenant: TenantContext, tenant: TenantContext,
@ -168,9 +182,9 @@ export const validateMandatoryBindMfa = async (
if (event === InteractionEvent.SignIn) { if (event === InteractionEvent.SignIn) {
const { accountId } = interaction; 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; return interaction;
} }

View file

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

View file

@ -16,6 +16,7 @@ create table users (
application_id varchar(21), application_id varchar(21),
identities jsonb /* @use Identities */ not null default '{}'::jsonb, identities jsonb /* @use Identities */ not null default '{}'::jsonb,
custom_data jsonb /* @use JsonObject */ 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, mfa_verifications jsonb /* @use MfaVerifications */ not null default '[]'::jsonb,
is_suspended boolean not null default false, is_suspended boolean not null default false,
last_sign_in_at timestamptz, last_sign_in_at timestamptz,