@@ -164,34 +164,34 @@ public function login(
164164 // This will be used to come back from the AuthSource login or from the Processing Chain
165165 $ returnToUrl = $ this ->getReturnUrl ($ request , $ sessionTicket );
166166
167- // Authenticate
168- if (
169- $ requestForceAuthenticate || !$ this ->authSource ->isAuthenticated ()
170- ) {
171- $ params = [
172- 'ForceAuthn ' => $ forceAuthn ,
173- 'isPassive ' => $ gateway ,
174- 'ReturnTo ' => $ returnToUrl ,
175- ];
176-
177- if (isset ($ entityId )) {
178- $ params ['saml:idp ' ] = $ entityId ;
179- }
167+ // Use case 4: renew=true and gateway=true are incompatible → prefer interactive login (disable passive)
168+ if ($ gateway && $ forceAuthn ) {
169+ $ gateway = false ;
170+ }
180171
181- if (isset ($ this ->idpList )) {
182- if (count ($ this ->idpList ) > 1 ) {
183- $ params ['saml:IDPList ' ] = $ this ->idpList ;
184- } else {
185- $ params ['saml:idp ' ] = $ this ->idpList [0 ];
186- }
172+ // Handle passive authentication
173+ if ($ gateway && !$ this ->authSource ->isAuthenticated () && !$ requestForceAuthenticate ) {
174+ $ gwResult = $ this ->handleUnauthenticatedGateway (
175+ $ request ,
176+ $ serviceUrl ,
177+ $ entityId ,
178+ $ returnToUrl ,
179+ );
180+ $ gateway = $ gwResult ['gateway ' ];
181+ if ($ gwResult ['response ' ] !== null ) {
182+ return $ gwResult ['response ' ];
187183 }
184+ }
188185
189- /*
190- * REDIRECT TO AUTHSOURCE LOGIN
191- * */
192- return new RunnableResponse (
193- [$ this ->authSource , 'login ' ],
194- [$ params ],
186+ // Handle interactive authentication
187+ if (
188+ $ requestForceAuthenticate || !$ this ->authSource ->isAuthenticated ()
189+ ) {
190+ return $ this ->handleInteractiveAuthenticate (
191+ forceAuthn: $ forceAuthn ,
192+ gateway: $ gateway ,
193+ returnToUrl: $ returnToUrl ,
194+ entityId: $ entityId ,
195195 );
196196 }
197197
@@ -204,9 +204,8 @@ public function login(
204204 $ this ->ticketStore ->addTicket ($ sessionTicket );
205205 }
206206
207- /*
208- * We are done. REDIRECT TO LOGGEDIN
209- * */
207+ /* We are done. REDIRECT TO LOGGEDIN */
208+
210209 if (!isset ($ serviceUrl ) && $ this ->authProcId === null ) {
211210 $ loggedInUrl = Module::getModuleURL ('casserver/loggedIn ' );
212211 return new RunnableResponse (
@@ -251,6 +250,7 @@ public function login(
251250 return $ t ;
252251 }
253252
253+ // Use case 1: user has SSO or non-interactive auth succeeded → redirect/POST to service WITH a ticket
254254 $ ticketName = $ this ->calculateTicketName ($ service );
255255 $ this ->postAuthUrlParameters [$ ticketName ] = $ serviceTicket ['id ' ];
256256
@@ -464,4 +464,126 @@ private function instantiateClassDependencies(): void
464464 // Attribute Extractor
465465 $ this ->attributeExtractor = new AttributeExtractor ($ this ->casConfig , $ processingChainFactory );
466466 }
467+
468+ /**
469+ * Trigger interactive authentication via the AuthSource.
470+ *
471+ * @param bool $forceAuthn
472+ * @param bool $gateway
473+ * @param string $returnToUrl
474+ * @param string|null $entityId
475+ *
476+ * @return RunnableResponse
477+ */
478+ private function handleInteractiveAuthenticate (
479+ bool $ forceAuthn ,
480+ bool $ gateway ,
481+ string $ returnToUrl ,
482+ ?string $ entityId ,
483+ ): RunnableResponse {
484+ $ params = [
485+ 'ForceAuthn ' => $ forceAuthn ,
486+ 'isPassive ' => $ gateway ,
487+ 'ReturnTo ' => $ returnToUrl ,
488+ ];
489+
490+ if (isset ($ entityId )) {
491+ $ params ['saml:idp ' ] = $ entityId ;
492+ }
493+
494+ if (isset ($ this ->idpList )) {
495+ if (sizeof ($ this ->idpList ) > 1 ) {
496+ $ params ['saml:IDPList ' ] = $ this ->idpList ;
497+ } else {
498+ $ params ['saml:idp ' ] = $ this ->idpList [0 ];
499+ }
500+ }
501+
502+ return new RunnableResponse (
503+ [$ this ->authSource , 'login ' ],
504+ [$ params ],
505+ );
506+ }
507+
508+
509+ /**
510+ * Handle the gateway flow when the user is NOT authenticated.
511+ * Passive mode is only attempted if 'enable_passive_mode' is enabled in configuration.
512+ *
513+ * Returns:
514+ * - ['response' => RunnableResponse|null, 'gateway' => bool] where 'gateway' may be toggled off for scenario 3.
515+ */
516+ private function handleUnauthenticatedGateway (
517+ Request $ request ,
518+ ?string $ serviceUrl ,
519+ ?string $ entityId ,
520+ string $ returnToUrl ,
521+ ): array {
522+ $ passiveAllowed = $ this ->casConfig ->getOptionalBoolean ('enable_passive_mode ' , false );
523+
524+ // If passive is not enabled by configuration, follow scenario 2/3 directly.
525+ if (!$ passiveAllowed ) {
526+ if ($ serviceUrl !== null ) {
527+ // Scenario 2: redirect to service WITHOUT any CAS parameters (always via GET redirect)
528+ return [
529+ 'response ' => new RunnableResponse (
530+ [$ this ->httpUtils , 'redirectTrustedURL ' ],
531+ [$ serviceUrl , []],
532+ ),
533+ 'gateway ' => true ,
534+ ];
535+ }
536+ // Scenario 3: no service → disable gateway and proceed with interactive login
537+ return ['response ' => null , 'gateway ' => false ];
538+ }
539+
540+ // Passive mode enabled: try a passive (non-interactive) authentication once
541+ $ gatewayTried = $ this ->getRequestParam ($ request , 'gatewayTried ' );
542+ if ($ gatewayTried !== '1 ' ) {
543+ $ rt = str_contains ($ returnToUrl , 'gatewayTried= ' )
544+ ? $ returnToUrl
545+ : $ returnToUrl . (str_contains ($ returnToUrl , '? ' ) ? '& ' : '? ' ) . 'gatewayTried=1 ' ;
546+
547+ $ passiveParams = [
548+ 'ForceAuthn ' => false ,
549+ 'isPassive ' => true ,
550+ 'ReturnTo ' => $ rt ,
551+ ];
552+
553+ if (isset ($ entityId )) {
554+ $ passiveParams ['saml:idp ' ] = $ entityId ;
555+ }
556+
557+ if (isset ($ this ->idpList )) {
558+ if (sizeof ($ this ->idpList ) > 1 ) {
559+ $ passiveParams ['saml:IDPList ' ] = $ this ->idpList ;
560+ } else {
561+ $ passiveParams ['saml:idp ' ] = $ this ->idpList [0 ];
562+ }
563+ }
564+
565+ return [
566+ 'response ' => new RunnableResponse (
567+ [$ this ->authSource , 'login ' ],
568+ [$ passiveParams ],
569+ ),
570+ 'gateway ' => true ,
571+ ];
572+ }
573+
574+ // Passive attempt already performed and still not authenticated.
575+ if ($ serviceUrl !== null ) {
576+ // Scenario 2: redirect to service WITHOUT any CAS parameters (always via GET redirect)
577+ return [
578+ 'response ' => new RunnableResponse (
579+ [$ this ->httpUtils , 'redirectTrustedURL ' ],
580+ [$ serviceUrl , []],
581+ ),
582+ 'gateway ' => true ,
583+ ];
584+ }
585+
586+ // Scenario 3: no service provided → disable gateway and proceed with interactive login
587+ return ['response ' => null , 'gateway ' => false ];
588+ }
467589}
0 commit comments