@@ -10,6 +10,7 @@ import {
10
10
setEmailConnector ,
11
11
setSmsConnector ,
12
12
} from '#src/helpers/connector.js' ;
13
+ import { fulfillUserEmail } from '#src/helpers/experience/index.js' ;
13
14
import {
14
15
successfullySendVerificationCode ,
15
16
successfullyVerifyVerificationCode ,
@@ -19,33 +20,35 @@ import { resetMfaSettings } from '#src/helpers/sign-in-experience.js';
19
20
import { generateNewUserProfile } from '#src/helpers/user.js' ;
20
21
import { generatePhone } from '#src/utils.js' ;
21
22
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
+
22
45
describe ( 'Register interaction - optional additional MFA suggestion' , ( ) => {
23
46
beforeAll ( async ( ) => {
24
47
await clearConnectorsByTypes ( [ ConnectorType . Email , ConnectorType . Sms ] ) ;
25
48
await setEmailConnector ( ) ;
26
49
await setSmsConnector ( ) ;
27
50
// 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 ) ;
49
52
} ) ;
50
53
51
54
afterAll ( async ( ) => {
@@ -141,6 +144,76 @@ describe('Register interaction - optional additional MFA suggestion', () => {
141
144
await deleteUser ( userId ) ;
142
145
} ) ;
143
146
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
+
144
217
it ( 'should not suggest MFA after fulfilling phone verification when both email and SMS factors are enabled' , async ( ) => {
145
218
// Configure MFA with email, phone, and TOTP factors
146
219
await updateSignInExperience ( {
0 commit comments