Skip to content

Commit d822a6c

Browse files
authored
feat(core,experience): show masked email/phone for MFA verification c… (#7699)
feat(core,experience): show masked email/phone for MFA verification code send
1 parent 0496a54 commit d822a6c

File tree

26 files changed

+84
-48
lines changed

26 files changed

+84
-48
lines changed

packages/core/src/routes/experience/classes/experience-interaction.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable max-lines */
22
import { appInsights } from '@logto/app-insights/node';
3-
import { InteractionEvent, VerificationType, type User } from '@logto/schemas';
3+
import { InteractionEvent, MfaFactor, VerificationType, type User } from '@logto/schemas';
4+
import { maskEmail, maskPhone } from '@logto/shared';
45
import { conditional, trySafe } from '@silverhand/essentials';
56

67
import RequestError from '#src/errors/RequestError/index.js';
@@ -339,11 +340,29 @@ export default class ExperienceInteraction {
339340
const mfaValidator = new MfaValidator(mfaSettings, user);
340341
const isVerified = mfaValidator.isMfaVerified(this.verificationRecordsArray);
341342

343+
const { primaryEmail, primaryPhone } = user;
344+
// Build masked identifiers for UX hints when applicable
345+
const maskedIdentifiers: Record<string, string> = {
346+
...(mfaValidator.availableUserMfaVerificationTypes.includes(
347+
MfaFactor.EmailVerificationCode
348+
) && primaryEmail
349+
? { [MfaFactor.EmailVerificationCode]: maskEmail(primaryEmail) }
350+
: {}),
351+
...(mfaValidator.availableUserMfaVerificationTypes.includes(
352+
MfaFactor.PhoneVerificationCode
353+
) && primaryPhone
354+
? { [MfaFactor.PhoneVerificationCode]: maskPhone(primaryPhone) }
355+
: {}),
356+
};
357+
342358
assertThat(
343359
isVerified,
344360
new RequestError(
345361
{ code: 'session.mfa.require_mfa_verification', status: 403 },
346-
{ availableFactors: mfaValidator.availableUserMfaVerificationTypes }
362+
{
363+
availableFactors: mfaValidator.availableUserMfaVerificationTypes,
364+
maskedIdentifiers,
365+
}
347366
)
348367
);
349368
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export const verifyMfa = async (
110110
const mfaData = userMfaDataGuard.safeParse(logtoConfig[userMfaDataKey]);
111111
const skipMfaOnSignIn = mfaData.success ? mfaData.data.skipMfaOnSignIn : undefined;
112112
const canSkipMfa = skipMfaOnSignIn && policy !== MfaPolicy.Mandatory;
113+
113114
assertThat(
114115
Boolean(canSkipMfa) || Boolean(verifiedMfa),
115116
new RequestError(

packages/experience/src/hooks/use-mfa-error-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const useMfaErrorHandler = ({ replace }: Options = {}) => {
106106
const [_, data] = validate(error.data, mfaErrorDataGuard);
107107
const factors = data?.availableFactors ?? [];
108108
const skippable = data?.skippable;
109+
const maskedIdentifiers = data?.maskedIdentifiers;
109110

110111
if (factors.length === 0) {
111112
setToast(error.message);
@@ -118,7 +119,7 @@ const useMfaErrorHandler = ({ replace }: Options = {}) => {
118119
? factors.filter((factor) => factor !== MfaFactor.WebAuthn)
119120
: factors;
120121

121-
await handleMfaRedirect(flow, { availableFactors, skippable });
122+
await handleMfaRedirect(flow, { availableFactors, skippable, maskedIdentifiers });
122123
};
123124
},
124125
[handleMfaRedirect, setToast]

packages/experience/src/pages/MfaVerification/EmailVerificationCode/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SignInIdentifier } from '@logto/schemas';
1+
import { MfaFactor, SignInIdentifier } from '@logto/schemas';
22
import { useEffect, useState } from 'react';
33

44
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
@@ -33,11 +33,14 @@ const EmailVerificationCode = () => {
3333
return <ErrorPage title="error.invalid_session" />;
3434
}
3535

36+
const maskedEmail = flowState.maskedIdentifiers?.[MfaFactor.EmailVerificationCode];
37+
3638
return (
3739
<SecondaryPageLayout title="mfa.verify_mfa_factors">
3840
<SectionLayout
3941
title="mfa.enter_email_verification_code"
4042
description="mfa.enter_email_verification_code_description"
43+
descriptionProps={{ identifier: maskedEmail }}
4144
>
4245
{verificationId ? (
4346
<MfaCodeVerification

packages/experience/src/pages/MfaVerification/PhoneVerificationCode/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SignInIdentifier } from '@logto/schemas';
1+
import { MfaFactor, SignInIdentifier } from '@logto/schemas';
22
import { useEffect, useState } from 'react';
33

44
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
@@ -33,11 +33,14 @@ const PhoneVerificationCode = () => {
3333
return <ErrorPage title="error.invalid_session" />;
3434
}
3535

36+
const maskedPhone = flowState.maskedIdentifiers?.[MfaFactor.PhoneVerificationCode];
37+
3638
return (
3739
<SecondaryPageLayout title="mfa.verify_mfa_factors">
3840
<SectionLayout
3941
title="mfa.enter_phone_verification_code"
4042
description="mfa.enter_phone_verification_code_description"
43+
descriptionProps={{ identifier: maskedPhone }}
4144
>
4245
{verificationId ? (
4346
<MfaCodeVerification

packages/experience/src/types/guard.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,15 @@ const mfaFactorsGuard = s.array(
6969
])
7070
);
7171

72+
const mfaFactorEnumValues = [
73+
MfaFactor.EmailVerificationCode,
74+
MfaFactor.PhoneVerificationCode,
75+
] as const;
76+
7277
export const mfaErrorDataGuard = s.object({
7378
availableFactors: mfaFactorsGuard,
7479
skippable: s.optional(s.boolean()),
80+
maskedIdentifiers: s.optional(s.record(s.enums(mfaFactorEnumValues), s.string())),
7581
});
7682

7783
export const mfaFlowStateGuard = mfaErrorDataGuard;
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1 @@
1-
export const maskEmail = (email: string) => {
2-
const [name = '', domain = ''] = email.split('@');
3-
4-
const preview = name.length > 4 ? `${name.slice(0, 4)}` : '';
5-
6-
return `${preview}****@${domain}`;
7-
};
8-
9-
export const maskPhone = (phone: string) => `****${phone.slice(-4)}`;
1+
export { maskEmail, maskPhone } from '@logto/shared/universal';

packages/phrases-experience/src/locales/ar/mfa.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ const mfa = {
3838
'تم تمكين التحقق من خطوتين لهذا الحساب. يرجى إدخال الرمز لمرة واحدة المعروض على تطبيق المصادقة المرتبط بك.',
3939
enter_email_verification_code: 'أدخل رمز التحقق عبر البريد الإلكتروني',
4040
enter_email_verification_code_description:
41-
'تم تمكين المصادقة بخطوتين لهذا الحساب. يرجى إدخال رمز التحقق المرسل إلى عنوان بريدك الإلكتروني.',
41+
'تم تمكين المصادقة بخطوتين لهذا الحساب. يرجى إدخال رمز التحقق المرسل إلى {{identifier}}.',
4242
enter_phone_verification_code: 'أدخل رمز التحقق عبر الرسائل القصيرة',
4343
enter_phone_verification_code_description:
44-
'تم تمكين المصادقة بخطوتين لهذا الحساب. يرجى إدخال رمز التحقق عبر الرسائل القصيرة المرسل إلى رقم هاتفك.',
44+
'تم تمكين المصادقة بخطوتين لهذا الحساب. يرجى إدخال رمز التحقق عبر الرسائل القصيرة المرسل إلى {{identifier}}.',
4545
link_another_mfa_factor: 'التبديل إلى طريقة أخرى',
4646
save_backup_code: 'احفظ رمز النسخ الاحتياطي الخاص بك',
4747
save_backup_code_description:

packages/phrases-experience/src/locales/de/mfa.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ const mfa = {
3939
'Für dieses Konto wurde die Zwei-Faktor-Authentifizierung aktiviert. Bitte geben Sie den einmaligen Code ein, der in Ihrer verknüpften Authentifizierungs-App angezeigt wird.',
4040
enter_email_verification_code: 'E-Mail‑Bestätigungscode eingeben',
4141
enter_email_verification_code_description:
42-
'Für dieses Konto ist die Zwei‑Faktor‑Authentifizierung aktiviert. Bitte geben Sie den an Ihre E‑Mail‑Adresse gesendeten Bestätigungscode ein.',
42+
'Für dieses Konto ist die Zwei‑Faktor‑Authentifizierung aktiviert. Bitte geben Sie den an {{identifier}} gesendeten Bestätigungscode ein.',
4343
enter_phone_verification_code: 'SMS‑Bestätigungscode eingeben',
4444
enter_phone_verification_code_description:
45-
'Für dieses Konto ist die Zwei‑Faktor‑Authentifizierung aktiviert. Bitte geben Sie den per SMS an Ihre Telefonnummer gesendeten Bestätigungscode ein.',
45+
'Für dieses Konto ist die Zwei‑Faktor‑Authentifizierung aktiviert. Bitte geben Sie den per SMS an {{identifier}} gesendeten Bestätigungscode ein.',
4646
link_another_mfa_factor: 'Zu einer anderen Methode wechseln',
4747
save_backup_code: 'Backup-Code speichern',
4848
save_backup_code_description:

packages/phrases-experience/src/locales/en/mfa.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ const mfa = {
3939
'2-step verification has been enabled for this account. Please enter the one-time code shown on your linked authenticator app.',
4040
enter_email_verification_code: 'Enter Email verification code',
4141
enter_email_verification_code_description:
42-
'2-step authentication has been enabled for this account. Please enter the email verification code sent to your email address.',
42+
'2-step authentication has been enabled for this account. Please enter the email verification code sent to {{identifier}}.',
4343
enter_phone_verification_code: 'Enter SMS verification code',
4444
enter_phone_verification_code_description:
45-
'2-step authentication has been enabled for this account. Please enter the SMS verification code sent to your phone number.',
45+
'2-step authentication has been enabled for this account. Please enter the SMS verification code sent to {{identifier}}.',
4646
link_another_mfa_factor: 'Switch to another method',
4747
save_backup_code: 'Save your backup code',
4848
save_backup_code_description:

0 commit comments

Comments
 (0)