0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -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: {
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,

View file

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

View file

@ -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,
},

View file

@ -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,
},

View file

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

View file

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

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),
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,