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:
parent
9ea79a18d6
commit
b118fc54a6
8 changed files with 54 additions and 10 deletions
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue