@@ -10,6 +10,7 @@ import {
1010 setEmailConnector ,
1111 setSmsConnector ,
1212} from '#src/helpers/connector.js' ;
13+ import { fulfillUserEmail } from '#src/helpers/experience/index.js' ;
1314import {
1415 successfullySendVerificationCode ,
1516 successfullyVerifyVerificationCode ,
@@ -19,33 +20,35 @@ import { resetMfaSettings } from '#src/helpers/sign-in-experience.js';
1920import { generateNewUserProfile } from '#src/helpers/user.js' ;
2021import { 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+
2245describe ( '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