Skip to content

Commit f71834b

Browse files
committed
Further simplify the code
1 parent 06e51f7 commit f71834b

1 file changed

Lines changed: 96 additions & 81 deletions

File tree

src/Controller/LoginController.php

Lines changed: 96 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)