Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion config/module_casserver.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ $config = [
'service_ticket_expire_time' => 5,
// how many seconds proxy granting tickets are valid for at most, defaults to 3600
'proxy_granting_ticket_expire_time' => 600,
//how many seconds proxy tickets are valid for, defaults to 5
// how many seconds proxy tickets are valid for, defaults to 5
'proxy_ticket_expire_time' => 5,
// OPTIONAL, if `gateway=true` is requested and user has no session, invoke the authsource with `isPassive=true`.
// defaults to false
//'enable_passive_mode' => true,

// If query param debugMode=true is sent to the login endpoint then print cas ticket xml. Default false
'debugMode' => true,
Expand Down
1 change: 1 addition & 0 deletions docker/ssp/authsources.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'users' => [
'student:studentpass' => [
'uid' => ['student'],
'cn' => ['Firsty Lasty'],
'eduPersonAffiliation' => ['member', 'student'],
'eduPersonNickname' => 'Sir_Nickname',
'displayName' => 'Some User',
Expand Down
2 changes: 1 addition & 1 deletion docker/ssp/module_casserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
url query parameter to CAS logout mandatory for obvious reasons.*/

// how many seconds service tickets are valid for, defaults to 5
'service_ticket_expire_time' => 5,
'service_ticket_expire_time' => 60,
// how many seconds proxy granting tickets are valid for at most, defaults to 3600
'proxy_granting_ticket_expire_time' => 600,
//how many seconds proxy tickets are valid for, defaults to 5
Expand Down
148 changes: 120 additions & 28 deletions src/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,34 +164,31 @@ public function login(
// This will be used to come back from the AuthSource login or from the Processing Chain
$returnToUrl = $this->getReturnUrl($request, $sessionTicket);

// Authenticate
// renew=true and gateway=true are incompatible → prefer interactive login (disable passive)
if ($gateway && $forceAuthn) {
$gateway = false;
}

// Handle passive authentication if service url defined
// Protocol (gateway set): CAS MUST NOT prompt for credentials during this branch.
if ($serviceUrl && $gateway && !$this->authSource->isAuthenticated() && !$requestForceAuthenticate) {
return $this->handleUnauthenticatedGateway(
$serviceUrl,
$entityId,
$returnToUrl,
);
}

// Handle interactive authentication
// Protocol: Normal interactive authentication flow (applies when gateway is not in effect).
// Renew semantics: when renew=true, server MUST enforce re-authentication (no SSO reuse).
if (
$requestForceAuthenticate || !$this->authSource->isAuthenticated()
) {
$params = [
'ForceAuthn' => $forceAuthn,
'isPassive' => $gateway,
'ReturnTo' => $returnToUrl,
];

if (isset($entityId)) {
$params['saml:idp'] = $entityId;
}

if (isset($this->idpList)) {
if (count($this->idpList) > 1) {
$params['saml:IDPList'] = $this->idpList;
} else {
$params['saml:idp'] = $this->idpList[0];
}
}

/*
* REDIRECT TO AUTHSOURCE LOGIN
* */
return new RunnableResponse(
[$this->authSource, 'login'],
[$params],
return $this->handleInteractiveAuthenticate(
forceAuthn: $forceAuthn,
returnToUrl: $returnToUrl,
entityId: $entityId,
);
}

Expand All @@ -204,9 +201,8 @@ public function login(
$this->ticketStore->addTicket($sessionTicket);
}

/*
* We are done. REDIRECT TO LOGGEDIN
* */
/* We are done. REDIRECT TO LOGGEDIN */

if (!isset($serviceUrl) && $this->authProcId === null) {
$loggedInUrl = Module::getModuleURL('casserver/loggedIn');
return new RunnableResponse(
Expand Down Expand Up @@ -251,6 +247,7 @@ public function login(
return $t;
}

// User has SSO or non-interactive auth succeeded → redirect/POST to service WITH a ticket
$ticketName = $this->calculateTicketName($service);
$this->postAuthUrlParameters[$ticketName] = $serviceTicket['id'];

Expand Down Expand Up @@ -464,4 +461,99 @@ private function instantiateClassDependencies(): void
// Attribute Extractor
$this->attributeExtractor = new AttributeExtractor($this->casConfig, $processingChainFactory);
}

/**
* Trigger interactive authentication via the AuthSource.
*
* @param bool $forceAuthn
* @param string $returnToUrl
* @param string|null $entityId
*
* @return RunnableResponse
*/
private function handleInteractiveAuthenticate(
bool $forceAuthn,
string $returnToUrl,
?string $entityId,
): RunnableResponse {
return $this->handleAuthenticate(
forceAuthn: $forceAuthn,
gateway: false,
returnToUrl: $returnToUrl,
entityId: $entityId,
);
}

/**
* Handle the gateway flow when the user is NOT authenticated.
* Passive mode is only attempted if 'enable_passive_mode' is enabled in configuration.
*
* Returns: RunnableResponse|null
* - RunnableResponse for either a passive attempt or a redirect to service without ticket.
* - null to indicate: proceed with interactive login (non-passive).
*/
private function handleUnauthenticatedGateway(
string $serviceUrl,
?string $entityId,
string $returnToUrl,
): RunnableResponse {
$passiveAllowed = $this->casConfig->getOptionalBoolean('enable_passive_mode', false);

// Passive mode is not enabled by configuration
// CAS MUST redirect to the service URL WITHOUT a ticket parameter.
if (!$passiveAllowed) {
return new RunnableResponse(
[$this->httpUtils, 'redirectTrustedURL'],
[$serviceUrl, []],
);
}

// Passive mode enabled: attempt a passive (non-interactive) authentication.
return $this->handleAuthenticate(
forceAuthn: false,
gateway: true,
returnToUrl: $returnToUrl,
entityId: $entityId,
);
}

/**
* Handle authentication request by configuring parameters and triggering login via auth source.
*
* @param bool $forceAuthn Whether to force authentication regardless of existing session
* @param bool $gateway Whether authentication should be passive/non-interactive
* @param string $returnToUrl URL to return to after authentication
* @param string|null $entityId Optional specific IdP entity ID to use
*
* @return RunnableResponse Response containing the login redirect
*/
private function handleAuthenticate(
bool $forceAuthn,
bool $gateway,
string $returnToUrl,
?string $entityId,
): RunnableResponse {
$params = [
'ForceAuthn' => $forceAuthn,
'isPassive' => $gateway,
'ReturnTo' => $returnToUrl,
];

if (isset($entityId)) {
$params['saml:idp'] = $entityId;
}

if (isset($this->idpList)) {
if (sizeof($this->idpList) > 1) {
$params['saml:IDPList'] = $this->idpList;
} else {
$params['saml:idp'] = $this->idpList[0];
}
}

return new RunnableResponse(
[$this->authSource, 'login'],
[$params],
);
}
}
Loading