Skip to content

Commit d523ba6

Browse files
authored
fix(core): respect secondary sign-up when suggesting MFA (#7828)
1 parent eeeba78 commit d523ba6

File tree

2 files changed

+98
-23
lines changed

2 files changed

+98
-23
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,14 +321,16 @@ export class Mfa {
321321
// If the user has email, but not registered by email, no suggestion
322322
if (
323323
factorsInUser.includes(MfaFactor.EmailVerificationCode) &&
324-
!signUp.identifiers.includes(SignInIdentifier.Email)
324+
!signUp.identifiers.includes(SignInIdentifier.Email) &&
325+
!signUp.secondaryIdentifiers?.some(({ identifier }) => identifier === SignInIdentifier.Email)
325326
) {
326327
return;
327328
}
328329
// If the user has phone, but not registered by phone, no suggestion
329330
if (
330331
factorsInUser.includes(MfaFactor.PhoneVerificationCode) &&
331-
!signUp.identifiers.includes(SignInIdentifier.Phone)
332+
!signUp.identifiers.includes(SignInIdentifier.Phone) &&
333+
!signUp.secondaryIdentifiers?.some(({ identifier }) => identifier === SignInIdentifier.Phone)
332334
) {
333335
return;
334336
}

packages/integration-tests/src/tests/api/experience-api/bind-mfa/mfa-suggestion.test.ts

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
setEmailConnector,
1111
setSmsConnector,
1212
} from '#src/helpers/connector.js';
13+
import { fulfillUserEmail } from '#src/helpers/experience/index.js';
1314
import {
1415
successfullySendVerificationCode,
1516
successfullyVerifyVerificationCode,
@@ -19,33 +20,35 @@ import { resetMfaSettings } from '#src/helpers/sign-in-experience.js';
1920
import { generateNewUserProfile } from '#src/helpers/user.js';
2021
import { generatePhone } from '#src/utils.js';
2122

23+
const emailPrimarySignInExperience = {
24+
signUp: {
25+
identifiers: [SignInIdentifier.Email],
26+
password: true,
27+
verify: true,
28+
},
29+
signIn: {
30+
methods: [
31+
{
32+
identifier: SignInIdentifier.Email,
33+
password: true,
34+
verificationCode: false,
35+
isPasswordPrimary: false,
36+
},
37+
],
38+
},
39+
mfa: {
40+
factors: [MfaFactor.EmailVerificationCode, MfaFactor.TOTP],
41+
policy: MfaPolicy.Mandatory,
42+
},
43+
};
44+
2245
describe('Register interaction - optional additional MFA suggestion', () => {
2346
beforeAll(async () => {
2447
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
2548
await setEmailConnector();
2649
await setSmsConnector();
2750
// Set up sign-in experience upfront (refer to email-with-signup.test.ts pattern)
28-
await updateSignInExperience({
29-
signUp: {
30-
identifiers: [SignInIdentifier.Email],
31-
password: true,
32-
verify: true,
33-
},
34-
signIn: {
35-
methods: [
36-
{
37-
identifier: SignInIdentifier.Email,
38-
password: true,
39-
verificationCode: false,
40-
isPasswordPrimary: false,
41-
},
42-
],
43-
},
44-
mfa: {
45-
factors: [MfaFactor.EmailVerificationCode, MfaFactor.TOTP],
46-
policy: MfaPolicy.Mandatory,
47-
},
48-
});
51+
await updateSignInExperience(emailPrimarySignInExperience);
4952
});
5053

5154
afterAll(async () => {
@@ -141,6 +144,76 @@ describe('Register interaction - optional additional MFA suggestion', () => {
141144
await deleteUser(userId);
142145
});
143146

147+
it('should suggest additional MFA when email is required as a secondary identifier', async () => {
148+
const secondaryEmailExperience = {
149+
signUp: {
150+
identifiers: [SignInIdentifier.Username],
151+
password: true,
152+
verify: true,
153+
secondaryIdentifiers: [
154+
{
155+
identifier: SignInIdentifier.Email,
156+
verify: true,
157+
},
158+
],
159+
},
160+
signIn: {
161+
methods: [
162+
{
163+
identifier: SignInIdentifier.Username,
164+
password: true,
165+
verificationCode: false,
166+
isPasswordPrimary: false,
167+
},
168+
],
169+
},
170+
mfa: {
171+
factors: [MfaFactor.EmailVerificationCode, MfaFactor.TOTP],
172+
policy: MfaPolicy.Mandatory,
173+
},
174+
};
175+
176+
await updateSignInExperience(secondaryEmailExperience);
177+
178+
const { username, password, primaryEmail } = generateNewUserProfile({
179+
username: true,
180+
password: true,
181+
primaryEmail: true,
182+
});
183+
184+
const client = await initExperienceClient({ interactionEvent: InteractionEvent.Register });
185+
186+
await client.updateProfile({ type: SignInIdentifier.Username, value: username });
187+
await client.updateProfile({ type: 'password', value: password });
188+
189+
await fulfillUserEmail(client, primaryEmail);
190+
191+
await client.identifyUser();
192+
193+
await expectRejects<{
194+
availableFactors: MfaFactor[];
195+
skippable: boolean;
196+
maskedIdentifiers?: Record<string, string>;
197+
suggestion?: boolean;
198+
}>(client.submitInteraction(), {
199+
code: 'session.mfa.suggest_additional_mfa',
200+
status: 422,
201+
expectData: (data) => {
202+
expect(data.availableFactors).toEqual([MfaFactor.TOTP, MfaFactor.EmailVerificationCode]);
203+
expect(data.maskedIdentifiers).toBeDefined();
204+
expect(data.maskedIdentifiers?.[MfaFactor.EmailVerificationCode]).toMatch(/\*{4}/);
205+
expect(data.skippable).toBe(true);
206+
expect(data.suggestion).toBe(true);
207+
},
208+
});
209+
210+
await client.skipMfaSuggestion();
211+
212+
const { redirectTo } = await client.submitInteraction();
213+
const userId = await processSession(client, redirectTo);
214+
await deleteUser(userId);
215+
});
216+
144217
it('should not suggest MFA after fulfilling phone verification when both email and SMS factors are enabled', async () => {
145218
// Configure MFA with email, phone, and TOTP factors
146219
await updateSignInExperience({

0 commit comments

Comments
 (0)