@@ -132,6 +132,61 @@ describe('resolveMfaSetup — auto-skip when no MFA methods are offerable', () =
132132 } ) ;
133133} ) ;
134134
135+ // ── resolveMfaSetup: auto-skip threads requestId into redirect target ───────────────────────────
136+ //
137+ // Regression guard: when resolveMfaSetup auto-skips (no offerable MFA methods), the resulting
138+ // redirect target MUST carry the requestId so the OIDC/SAML ceremony can resume. Without it,
139+ // nextStepFromSession builds a target without the requestId query param → the handback to the
140+ // IdP is broken → staging returns /id/error?code=signin_failed after Google IdP login.
141+ //
142+ // REQUEST_ID_PATTERN in next-step only threads ids matching oidc_*/saml_*/device_* prefixes,
143+ // so we use 'oidc_test123' to exercise the actual threading path.
144+
145+ describe ( 'resolveMfaSetup — auto-skip threads requestId into the redirect target' , ( ) => {
146+ const NO_MFA_CAPS = {
147+ passkey : false ,
148+ u2f : false ,
149+ totpOtp : false ,
150+ emailOtp : false ,
151+ smsOtp : false ,
152+ externalIdp : false ,
153+ ldap : false ,
154+ saml : false ,
155+ oidc : false ,
156+ registration : false ,
157+ } ;
158+
159+ const scenarioWithRequestId = {
160+ fn : 'resolveMfaSetup' as const ,
161+ provider : 'fresh' as const ,
162+ seed : {
163+ users : [ { id : 'u-nomfa2' , loginName : 'nomfa2@test.example' } ] ,
164+ authMethods : { 'u-nomfa2' : [ 'password' ] as string [ ] } ,
165+ capabilities : NO_MFA_CAPS ,
166+ } ,
167+ liveSessions : [
168+ {
169+ id : 'sess-nomfa2' ,
170+ token : 'tok-nomfa2' ,
171+ user : { id : 'u-nomfa2' , loginName : 'nomfa2@test.example' } ,
172+ } ,
173+ ] ,
174+ request : {
175+ url : 'http://localhost/id/setup/mfa' ,
176+ sessions : [ { id : 'sess-nomfa2' , token : 'tok-nomfa2' , loginName : 'nomfa2@test.example' } ] ,
177+ } ,
178+ mfaInput : { loginName : 'nomfa2@test.example' , requestId : 'oidc_test123' } ,
179+ } ;
180+
181+ it ( 'redirect target includes requestId=oidc_test123 so the OIDC ceremony can resume' , ( ) => {
182+ callService ( scenarioWithRequestId ) . then ( ( v ) => {
183+ const o = v . outcome as { kind : string ; target ?: string } ;
184+ expect ( o . kind , 'auto-skip: kind must be redirect' ) . to . equal ( 'redirect' ) ;
185+ expect ( o . target ?? '' , 'target must carry requestId' ) . to . include ( 'requestId=oidc_test123' ) ;
186+ } ) ;
187+ } ) ;
188+ } ) ;
189+
135190// ── resolveMfaSetup: normal path (non-empty offerable set) still returns offerableKeys ──────────
136191//
137192// Regression guard: ensure the fix does not break the normal chooser path where MFA methods ARE
0 commit comments