@@ -164,32 +164,36 @@ 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- // Use case 4: renew=true and gateway=true are incompatible → prefer interactive login (disable passive)
167+ // renew=true and gateway=true are incompatible → prefer interactive login (disable passive)
168+ // Protocol: gateway and renew are incompatible; behavior is undefined if both are set.
169+ // OPTIONAL (implementation policy): Prefer renew (interactive, non-passive) by disabling gateway.
170+ // OPTIONAL alternative: Reject with 400 to signal incompatible parameters.
168171 if ($ gateway && $ forceAuthn ) {
169172 $ gateway = false ;
170173 }
171174
172175 // Handle passive authentication
176+ // Protocol (gateway set): CAS MUST NOT prompt for credentials during this branch.
173177 if ($ gateway && !$ this ->authSource ->isAuthenticated () && !$ requestForceAuthenticate ) {
174- $ gwResult = $ this ->handleUnauthenticatedGateway (
178+ $ response = $ this ->handleUnauthenticatedGateway (
175179 $ request ,
176180 $ serviceUrl ,
177181 $ entityId ,
178182 $ returnToUrl ,
179183 );
180- $ gateway = $ gwResult ['gateway ' ];
181- if ($ gwResult ['response ' ] !== null ) {
182- return $ gwResult ['response ' ];
184+ if ($ response !== null ) {
185+ return $ response ;
183186 }
184187 }
185188
186189 // Handle interactive authentication
190+ // Protocol: Normal interactive authentication flow (applies when gateway is not in effect).
191+ // Renew semantics: when renew=true, server MUST enforce re-authentication (no SSO reuse).
187192 if (
188193 $ requestForceAuthenticate || !$ this ->authSource ->isAuthenticated ()
189194 ) {
190195 return $ this ->handleInteractiveAuthenticate (
191196 forceAuthn: $ forceAuthn ,
192- gateway: $ gateway ,
193197 returnToUrl: $ returnToUrl ,
194198 entityId: $ entityId ,
195199 );
@@ -250,7 +254,10 @@ public function login(
250254 return $ t ;
251255 }
252256
253- // Use case 1: user has SSO or non-interactive auth succeeded → redirect/POST to service WITH a ticket
257+ // User has SSO or non-interactive auth succeeded → redirect/POST to service WITH a ticket
258+ // Protocol: With gateway and a successful non-interactive auth (or existing SSO), CAS MAY redirect to the
259+ // service and append a ticket.
260+ // Protocol: CAS MAY interpose an advisory page indicating that authentication took place.
254261 $ ticketName = $ this ->calculateTicketName ($ service );
255262 $ this ->postAuthUrlParameters [$ ticketName ] = $ serviceTicket ['id ' ];
256263
@@ -469,121 +476,129 @@ private function instantiateClassDependencies(): void
469476 * Trigger interactive authentication via the AuthSource.
470477 *
471478 * @param bool $forceAuthn
472- * @param bool $gateway
473479 * @param string $returnToUrl
474480 * @param string|null $entityId
475481 *
476482 * @return RunnableResponse
477483 */
478484 private function handleInteractiveAuthenticate (
479485 bool $ forceAuthn ,
480- bool $ gateway ,
481486 string $ returnToUrl ,
482487 ?string $ entityId ,
483488 ): 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 ],
489+ return $ this ->handleAuthenticate (
490+ forceAuthn: $ forceAuthn ,
491+ gateway: false ,
492+ returnToUrl: $ returnToUrl ,
493+ entityId: $ entityId ,
505494 );
506495 }
507496
508-
509497 /**
510498 * Handle the gateway flow when the user is NOT authenticated.
511499 * Passive mode is only attempted if 'enable_passive_mode' is enabled in configuration.
512500 *
513- * Returns:
514- * - ['response' => RunnableResponse|null, 'gateway' => bool] where 'gateway' may be toggled off for scenario 3.
501+ * Returns: RunnableResponse|null
502+ * - RunnableResponse for either a passive attempt or a redirect to service without ticket.
503+ * - null to indicate: proceed with interactive login (non-passive).
515504 */
516505 private function handleUnauthenticatedGateway (
517506 Request $ request ,
518507 ?string $ serviceUrl ,
519508 ?string $ entityId ,
520509 string $ returnToUrl ,
521- ): array {
510+ ): ? RunnableResponse {
522511 $ passiveAllowed = $ this ->casConfig ->getOptionalBoolean ('enable_passive_mode ' , false );
523512
524- // If passive is not enabled by configuration, follow scenario 2/3 directly.
513+ // Passive mode is not enabled by configuration.
514+ // Protocol: If non-interactive auth cannot be established:
515+ // - If service is present, CAS MUST redirect to the service URL WITHOUT a ticket parameter.
516+ // - If service is absent, behavior is undefined; it is RECOMMENDED
517+ // to request credentials as if neither parameter was specified.
525518 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 ];
519+ // Passive attempt already performed and still not authenticated.
520+ return $ this ->gatewayFallback ($ serviceUrl );
538521 }
539522
540523 // Passive mode enabled: try a passive (non-interactive) authentication once
524+ // OPTIONAL (implementation): Attempt passive exactly once while avoiding loops.
541525 $ gatewayTried = $ this ->getRequestParam ($ request , 'gatewayTried ' );
542526 if ($ gatewayTried !== '1 ' ) {
543527 $ rt = str_contains ($ returnToUrl , 'gatewayTried= ' )
544528 ? $ returnToUrl
545529 : $ returnToUrl . (str_contains ($ returnToUrl , '? ' ) ? '& ' : '? ' ) . 'gatewayTried=1 ' ;
546530
547- $ passiveParams = [
548- 'ForceAuthn ' => false ,
549- 'isPassive ' => true ,
550- 'ReturnTo ' => $ rt ,
551- ];
531+ return $ this ->handleAuthenticate (
532+ forceAuthn: false ,
533+ gateway: true ,
534+ returnToUrl: $ rt ,
535+ entityId: $ entityId ,
536+ );
537+ }
552538
553- if ( isset ( $ entityId )) {
554- $ passiveParams [ ' saml:idp ' ] = $ entityId ;
555- }
539+ // Passive attempt already performed and still not authenticated.
540+ return $ this -> gatewayFallback ( $ serviceUrl ) ;
541+ }
556542
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- }
543+ /**
544+ * Handle authentication request by configuring parameters and triggering login via auth source.
545+ *
546+ * @param bool $forceAuthn Whether to force authentication regardless of existing session
547+ * @param bool $gateway Whether authentication should be passive/non-interactive
548+ * @param string $returnToUrl URL to return to after authentication
549+ * @param string|null $entityId Optional specific IdP entity ID to use
550+ *
551+ * @return RunnableResponse Response containing the login redirect
552+ */
553+ private function handleAuthenticate (
554+ bool $ forceAuthn ,
555+ bool $ gateway ,
556+ string $ returnToUrl ,
557+ ?string $ entityId ,
558+ ): RunnableResponse {
559+ $ params = [
560+ 'ForceAuthn ' => $ forceAuthn ,
561+ 'isPassive ' => $ gateway ,
562+ 'ReturnTo ' => $ returnToUrl ,
563+ ];
564564
565- return [
566- 'response ' => new RunnableResponse (
567- [$ this ->authSource , 'login ' ],
568- [$ passiveParams ],
569- ),
570- 'gateway ' => true ,
571- ];
565+ if (isset ($ entityId )) {
566+ $ params ['saml:idp ' ] = $ entityId ;
572567 }
573568
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- ];
569+ if (isset ($ this ->idpList )) {
570+ if (sizeof ($ this ->idpList ) > 1 ) {
571+ $ params ['saml:IDPList ' ] = $ this ->idpList ;
572+ } else {
573+ $ params ['saml:idp ' ] = $ this ->idpList [0 ];
574+ }
584575 }
585576
586- // Scenario 3: no service provided → disable gateway and proceed with interactive login
587- return ['response ' => null , 'gateway ' => false ];
577+ return new RunnableResponse (
578+ [$ this ->authSource , 'login ' ],
579+ [$ params ],
580+ );
581+ }
582+
583+ /**
584+ * Gateway fallback per CAS gateway semantics:
585+ * - Protocol (MUST): If a service is provided and non-interactive auth cannot be established,
586+ * redirect to the service WITHOUT any CAS parameters (no "ticket").
587+ * - Protocol (Undefined, RECOMMENDED): If no service is provided, proceed with interactive login
588+ * (request credentials).
589+ * @param string|null $serviceUrl
590+ * @return RunnableResponse|null
591+ */
592+ private function gatewayFallback (?string $ serviceUrl ): ?RunnableResponse
593+ {
594+ if ($ serviceUrl !== null ) {
595+ // MUST: Redirect to service WITHOUT a "ticket" parameter (and without other CAS params).
596+ return new RunnableResponse (
597+ [$ this ->httpUtils , 'redirectTrustedURL ' ],
598+ [$ serviceUrl , []],
599+ );
600+ }
601+ // RECOMMENDED: No service specified; proceed with interactive login as if neither parameter was specified.
602+ return null ;
588603 }
589604}
0 commit comments