Skip to content

Commit 987a00d

Browse files
authored
refactor(core): remove unused email sms mfa code (#7669)
1 parent 0403429 commit 987a00d

File tree

11 files changed

+91
-226
lines changed

11 files changed

+91
-226
lines changed

packages/core/src/libraries/user.utils.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,6 @@ export const convertBindMfaToMfaVerification = (bindMfa: BindMfa): MfaVerificati
4747
};
4848
}
4949

50-
if (type === MfaFactor.EmailVerificationCode || type === MfaFactor.PhoneVerificationCode) {
51-
// TODO @wangsijie: Implement this later
52-
throw new Error('Not implemented yet');
53-
}
54-
5550
const { credentialId, counter, publicKey, transports, agent } = bindMfa;
5651
return {
5752
...base,

packages/core/src/routes/experience/classes/helpers.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,7 @@
66
*/
77

88
import { defaults, parseAffiliateData } from '@logto/affiliate';
9-
import {
10-
adminTenantId,
11-
MfaFactor,
12-
VerificationType,
13-
type User,
14-
type Mfa,
15-
type MfaVerification,
16-
} from '@logto/schemas';
9+
import { adminTenantId, MfaFactor, VerificationType, type User, type Mfa } from '@logto/schemas';
1710
import { conditional, trySafe } from '@silverhand/essentials';
1811
import { type IRouterContext } from 'koa-router';
1912

@@ -202,10 +195,33 @@ export const getAllUserEnabledMfaVerifications = (
202195
mfaSettings: Mfa,
203196
user: User,
204197
currentProfile?: InteractionProfile
205-
): MfaVerification[] => {
206-
const storedVerifications = filterOutEmptyBackupCodes(user.mfaVerifications).filter(
207-
(verification) => mfaSettings.factors.includes(verification.type)
208-
);
198+
): MfaFactor[] => {
199+
const storedVerifications = filterOutEmptyBackupCodes(user.mfaVerifications)
200+
.filter((verification) => mfaSettings.factors.includes(verification.type))
201+
// Filter out backup codes if all the codes are used
202+
.filter((verification) => {
203+
if (verification.type !== MfaFactor.BackupCode) {
204+
return true;
205+
}
206+
return verification.codes.some((code) => !code.usedAt);
207+
})
208+
.slice()
209+
// Sort by last used time, the latest used factor is the first one, backup code is always the last one
210+
.sort((verificationA, verificationB) => {
211+
if (verificationA.type === MfaFactor.BackupCode) {
212+
return 1;
213+
}
214+
215+
if (verificationB.type === MfaFactor.BackupCode) {
216+
return -1;
217+
}
218+
219+
return (
220+
new Date(verificationB.lastUsedAt ?? 0).getTime() -
221+
new Date(verificationA.lastUsedAt ?? 0).getTime()
222+
);
223+
})
224+
.map(({ type }) => type);
209225

210226
if (!EnvSet.values.isDevFeaturesEnabled) {
211227
return storedVerifications;
@@ -217,23 +233,11 @@ export const getAllUserEnabledMfaVerifications = (
217233
const implicitVerifications = [
218234
// Email MFA Factor: user has primaryEmail + Email factor enabled in SIE
219235
...(mfaSettings.factors.includes(MfaFactor.EmailVerificationCode) && email
220-
? ([
221-
{
222-
id: 'implicit-email-mfa', // Fake ID for capability
223-
type: MfaFactor.EmailVerificationCode,
224-
createdAt: new Date(user.createdAt).toISOString(),
225-
},
226-
] satisfies MfaVerification[])
236+
? [MfaFactor.EmailVerificationCode]
227237
: []),
228238
// Phone MFA Factor: user has primaryPhone + Phone factor enabled in SIE
229239
...(mfaSettings.factors.includes(MfaFactor.PhoneVerificationCode) && phone
230-
? ([
231-
{
232-
id: 'implicit-phone-mfa', // Fake ID for capability
233-
type: MfaFactor.PhoneVerificationCode,
234-
createdAt: new Date(user.createdAt).toISOString(),
235-
},
236-
] satisfies MfaVerification[])
240+
? [MfaFactor.PhoneVerificationCode]
237241
: []),
238242
];
239243

packages/core/src/routes/experience/classes/libraries/mfa-validator.ts

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
userMfaDataGuard,
66
userMfaDataKey,
77
type Mfa,
8-
type MfaVerification,
98
type User,
109
} from '@logto/schemas';
1110

@@ -82,38 +81,14 @@ export class MfaValidator {
8281
get availableUserMfaVerificationTypes() {
8382
return (
8483
this.userEnabledMfaVerifications
85-
// Filter out backup codes if all the codes are used
86-
.filter((verification) => {
87-
if (verification.type !== MfaFactor.BackupCode) {
88-
return true;
89-
}
90-
return verification.codes.some((code) => !code.usedAt);
91-
})
9284
// Filter out duplicated verifications with the same type
93-
.reduce<MfaVerification[]>((verifications, verification) => {
94-
if (verifications.some(({ type }) => type === verification.type)) {
85+
.reduce<MfaFactor[]>((verifications, verification) => {
86+
if (verifications.includes(verification)) {
9587
return verifications;
9688
}
9789

9890
return [...verifications, verification];
9991
}, [])
100-
.slice()
101-
// Sort by last used time, the latest used factor is the first one, backup code is always the last one
102-
.sort((verificationA, verificationB) => {
103-
if (verificationA.type === MfaFactor.BackupCode) {
104-
return 1;
105-
}
106-
107-
if (verificationB.type === MfaFactor.BackupCode) {
108-
return -1;
109-
}
110-
111-
return (
112-
new Date(verificationB.lastUsedAt ?? 0).getTime() -
113-
new Date(verificationA.lastUsedAt ?? 0).getTime()
114-
);
115-
})
116-
.map(({ type }) => type)
11792
);
11893
}
11994

@@ -146,8 +121,8 @@ export class MfaValidator {
146121
// New bind MFA verification can not be used for verification
147122
!verification.isNewBindMfaVerification &&
148123
// Check if the verification type is enabled in the user's MFA settings
149-
this.userEnabledMfaVerifications.some(
150-
(factor) => factor.type === mfaVerificationTypeToMfaFactorMap[verification.type]
124+
this.userEnabledMfaVerifications.includes(
125+
mfaVerificationTypeToMfaFactorMap[verification.type]
151126
)
152127
);
153128

packages/core/src/routes/experience/classes/mfa.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ export class Mfa {
391391
currentProfile
392392
);
393393
return [
394-
...existingVerifications.map(({ type }) => type),
394+
...existingVerifications,
395395
...(this.#totp ? [MfaFactor.TOTP] : []),
396396
...(this.#webAuthn?.length ? [MfaFactor.WebAuthn] : []),
397397
].filter(Boolean);

packages/core/src/routes/interaction/actions/submit-interaction.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,6 @@ const parseBindMfas = ({
6767
};
6868
}
6969

70-
if (
71-
bindMfa.type === MfaFactor.EmailVerificationCode ||
72-
bindMfa.type === MfaFactor.PhoneVerificationCode
73-
) {
74-
throw new Error('Not implemented yet');
75-
}
76-
7770
// MfaFactor.PhoneVerificationCode
7871
return {
7972
id: generateStandardId(),

packages/core/src/routes/interaction/verifications/mfa-payload-verification.ts

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,6 @@ export async function bindMfaPayloadVerification(
168168
return verifyBindWebAuthn(interactionStorage, bindMfaPayload, ctx, { rpId, userAgent, origin });
169169
}
170170

171-
if (
172-
bindMfaPayload.type === MfaFactor.EmailVerificationCode ||
173-
bindMfaPayload.type === MfaFactor.PhoneVerificationCode
174-
) {
175-
// TODO: Implement email and SMS verification code binding
176-
throw new Error('Email and SMS verification code MFA binding not implemented');
177-
}
178-
179171
return verifyBindBackupCode(interactionStorage, bindMfaPayload, ctx);
180172
}
181173

@@ -252,41 +244,30 @@ export async function verifyMfaPayloadVerification(
252244
return result;
253245
}
254246

255-
if (verifyMfaPayload.type === MfaFactor.BackupCode) {
256-
const { id, type } = await verifyBackupCode(user.mfaVerifications, verifyMfaPayload);
257-
258-
// Mark the backup code as used
259-
await tenant.queries.users.updateUserById(accountId, {
260-
mfaVerifications: user.mfaVerifications.map((mfa) => {
261-
if (mfa.id !== id || mfa.type !== MfaFactor.BackupCode) {
262-
return mfa;
263-
}
264-
265-
return {
266-
...mfa,
267-
codes: mfa.codes.map((code) => {
268-
if (code.code !== verifyMfaPayload.code) {
269-
return code;
270-
}
271-
272-
return {
273-
...code,
274-
usedAt: new Date().toISOString(),
275-
};
276-
}),
277-
};
278-
}),
279-
});
247+
const { id, type } = await verifyBackupCode(user.mfaVerifications, verifyMfaPayload);
280248

281-
return { id, type };
282-
}
249+
// Mark the backup code as used
250+
await tenant.queries.users.updateUserById(accountId, {
251+
mfaVerifications: user.mfaVerifications.map((mfa) => {
252+
if (mfa.id !== id || mfa.type !== MfaFactor.BackupCode) {
253+
return mfa;
254+
}
283255

284-
if (verifyMfaPayload.type === MfaFactor.EmailVerificationCode) {
285-
// TODO: Implement email verification code verification
286-
throw new Error('Email verification code MFA verification not implemented');
287-
}
256+
return {
257+
...mfa,
258+
codes: mfa.codes.map((code) => {
259+
if (code.code !== verifyMfaPayload.code) {
260+
return code;
261+
}
262+
263+
return {
264+
...code,
265+
usedAt: new Date().toISOString(),
266+
};
267+
}),
268+
};
269+
}),
270+
});
288271

289-
// MfaFactor.PhoneVerificationCode
290-
// TODO: Implement phone verification code verification
291-
throw new Error('Phone verification code MFA verification not implemented');
272+
return { id, type };
292273
}

packages/experience/src/apis/experience/mfa.ts

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -60,40 +60,40 @@ export const skipMfa = async () => {
6060
return submitInteraction();
6161
};
6262

63-
export const bindMfa = async (payload: BindMfaPayload, verificationId: string) => {
64-
switch (payload.type) {
65-
case MfaFactor.TOTP: {
66-
const { code } = payload;
67-
await api.post(`${experienceApiRoutes.verification}/totp/verify`, {
68-
json: {
69-
code,
70-
verificationId,
71-
},
72-
});
73-
break;
74-
}
75-
case MfaFactor.WebAuthn: {
76-
await api.post(`${experienceApiRoutes.verification}/web-authn/registration/verify`, {
77-
json: {
78-
verificationId,
79-
payload,
80-
},
81-
});
82-
break;
83-
}
84-
case MfaFactor.BackupCode: {
85-
// No need to verify backup codes
86-
break;
87-
}
88-
case MfaFactor.EmailVerificationCode:
89-
case MfaFactor.PhoneVerificationCode: {
90-
// Email/Phone MFA factors use special binding logic, but don't submit immediately
91-
// to allow additional MFA factors to be bound in the same session
92-
break;
63+
export const bindMfa = async (
64+
type: MfaFactor,
65+
verificationId: string,
66+
payload?: BindMfaPayload
67+
) => {
68+
if (payload) {
69+
switch (payload.type) {
70+
case MfaFactor.TOTP: {
71+
const { code } = payload;
72+
await api.post(`${experienceApiRoutes.verification}/totp/verify`, {
73+
json: {
74+
code,
75+
verificationId,
76+
},
77+
});
78+
break;
79+
}
80+
case MfaFactor.WebAuthn: {
81+
await api.post(`${experienceApiRoutes.verification}/web-authn/registration/verify`, {
82+
json: {
83+
verificationId,
84+
payload,
85+
},
86+
});
87+
break;
88+
}
89+
case MfaFactor.BackupCode: {
90+
// No need to verify backup codes
91+
break;
92+
}
9393
}
9494
}
9595

96-
await addMfa(payload.type, verificationId);
96+
await addMfa(type, verificationId);
9797
return submitInteraction();
9898
};
9999

@@ -126,11 +126,6 @@ export const verifyMfa = async (payload: VerifyMfaPayload, verificationId?: stri
126126
});
127127
break;
128128
}
129-
case MfaFactor.EmailVerificationCode:
130-
case MfaFactor.PhoneVerificationCode: {
131-
// TODO: Implement email and phone verification code verification
132-
break;
133-
}
134129
}
135130

136131
return submitInteraction();

packages/experience/src/containers/VerificationCode/use-continue-flow-code-verification.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ const useContinueFlowCodeVerification = (
141141
}
142142

143143
const [bindError, bindResult] = await asyncBindMfa(
144-
{ type: MfaFactor.EmailVerificationCode },
144+
MfaFactor.EmailVerificationCode,
145145
verificationId
146146
);
147147

packages/experience/src/hooks/use-send-mfa-payload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type SendMfaPayloadApiOptions =
2323

2424
const sendMfaPayloadApi = async ({ flow, payload, verificationId }: SendMfaPayloadApiOptions) => {
2525
if (flow === UserMfaFlow.MfaBinding) {
26-
return bindMfa(payload, verificationId);
26+
return bindMfa(payload.type, verificationId, payload);
2727
}
2828
return verifyMfa(payload, verificationId);
2929
};

packages/schemas/src/foundations/jsonb-types/users.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,31 +111,10 @@ export const mfaVerificationBackupCode = z.object({
111111

112112
export type MfaVerificationBackupCode = z.infer<typeof mfaVerificationBackupCode>;
113113

114-
// TODO @wangsijie: Implement this later
115-
export const mfaVerificationEmailVerificationCode = z.object({
116-
type: z.literal(MfaFactor.EmailVerificationCode),
117-
...baseMfaVerification,
118-
});
119-
120-
export type MfaVerificationEmailVerificationCode = z.infer<
121-
typeof mfaVerificationEmailVerificationCode
122-
>;
123-
124-
export const mfaVerificationPhoneVerificationCode = z.object({
125-
type: z.literal(MfaFactor.PhoneVerificationCode),
126-
...baseMfaVerification,
127-
});
128-
129-
export type MfaVerificationPhoneVerificationCode = z.infer<
130-
typeof mfaVerificationPhoneVerificationCode
131-
>;
132-
133114
export const mfaVerificationGuard = z.discriminatedUnion('type', [
134115
mfaVerificationTotp,
135116
mfaVerificationWebAuthn,
136117
mfaVerificationBackupCode,
137-
mfaVerificationEmailVerificationCode,
138-
mfaVerificationPhoneVerificationCode,
139118
]);
140119

141120
export type MfaVerification = z.infer<typeof mfaVerificationGuard>;

0 commit comments

Comments
 (0)