Skip to content

Commit 06e51f7

Browse files
committed
Improve gateway parameter handling to properly match the protocol description
1 parent 5dd74ac commit 06e51f7

3 files changed

Lines changed: 469 additions & 29 deletions

File tree

config/module_casserver.php.dist

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,10 @@ $config = [
115115
'service_ticket_expire_time' => 5,
116116
// how many seconds proxy granting tickets are valid for at most, defaults to 3600
117117
'proxy_granting_ticket_expire_time' => 600,
118-
//how many seconds proxy tickets are valid for, defaults to 5
118+
// how many seconds proxy tickets are valid for, defaults to 5
119119
'proxy_ticket_expire_time' => 5,
120+
// OPTIONAL, enable CAS passive mode, defaults to false
121+
//'enable_passive_mode' => true,
120122

121123
// If query param debugMode=true is sent to the login endpoint then print cas ticket xml. Default false
122124
'debugMode' => true,

src/Controller/LoginController.php

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

Comments
 (0)