Skip to content

Commit a6537a4

Browse files
pradtketvdijen
authored andcommitted
Merge commit from fork
* Support Cas v3 logout and validate logout url * Remove separate loggedout controller to avoid needing to duplicate url validation logic * Address PR feedback
1 parent ef118d9 commit a6537a4

File tree

6 files changed

+156
-107
lines changed

6 files changed

+156
-107
lines changed

docker/ssp/config-override.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
$config['module.enable']['casserver'] = true;
77
// Have preprod warning enabled (though it may not be installed) to ease authproc redirect testing
88
$config['module.enable']['preprodwarning'] = true;
9+
$config['trusted.url.domains'] = [ 'main-config.example.com'];
910
$config = [
1011
'secretsalt' => 'testsalt',
1112
'logging.level' => 7,

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,11 +310,6 @@ parameters:
310310
count: 1
311311
path: src/Controller/LoginController.php
312312

313-
-
314-
message: "#^Property SimpleSAML\\\\Module\\\\casserver\\\\Controller\\\\LogoutController\\:\\:\\$sspConfig is never read, only written\\.$#"
315-
count: 1
316-
path: src/Controller/LogoutController.php
317-
318313
-
319314
message: "#^Access to an undefined property DOMNode\\:\\:\\$value\\.$#"
320315
count: 1

routing/routes/routes.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@
3737
->methods(['POST']);
3838
$routes->add(RoutesEnum::Logout->name, RoutesEnum::Logout->value)
3939
->controller([LogoutController::class, 'logout']);
40+
// This path was combined into the main logout
4041
$routes->add(RoutesEnum::LoggedOut->name, RoutesEnum::LoggedOut->value)
41-
->controller([LoggedOutController::class, 'main']);
42+
->controller([LogoutController::class, 'logout']);
4243
$routes->add(RoutesEnum::Login->name, RoutesEnum::Login->value)
4344
->controller([LoginController::class, 'login']);
4445
$routes->add(RoutesEnum::LoggedIn->name, RoutesEnum::LoggedIn->value)
@@ -61,8 +62,9 @@
6162
->methods(['POST']);
6263
$routes->add(LegacyRoutesEnum::LegacyLogout->name, LegacyRoutesEnum::LegacyLogout->value)
6364
->controller([LogoutController::class, 'logout']);
65+
// This path was combined into the main logout
6466
$routes->add(LegacyRoutesEnum::LegacyLoggedOut->name, LegacyRoutesEnum::LegacyLoggedOut->value)
65-
->controller([LoggedOutController::class, 'main']);
67+
->controller([LogoutController::class, 'logout']);
6668
$routes->add(LegacyRoutesEnum::LegacyLogin->name, LegacyRoutesEnum::LegacyLogin->value)
6769
->controller([LoginController::class, 'login']);
6870
$routes->add(LegacyRoutesEnum::LegacyLoggedIn->name, LegacyRoutesEnum::LegacyLoggedIn->value)

src/Controller/LoggedOutController.php

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/Controller/LogoutController.php

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
use SimpleSAML\Logger;
1212
use SimpleSAML\Module;
1313
use SimpleSAML\Module\casserver\Cas\Factories\TicketFactory;
14+
use SimpleSAML\Module\casserver\Cas\ServiceValidator;
1415
use SimpleSAML\Module\casserver\Cas\Ticket\TicketStore;
1516
use SimpleSAML\Module\casserver\Controller\Traits\UrlTrait;
1617
use SimpleSAML\Session;
1718
use SimpleSAML\Utils;
19+
use SimpleSAML\XHTML\Template;
1820
use Symfony\Component\HttpFoundation\Request;
1921
use Symfony\Component\HttpKernel\Attribute\AsController;
2022
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
@@ -41,6 +43,7 @@ class LogoutController
4143

4244
/** @var \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore */
4345
protected TicketStore $ticketStore;
46+
private ServiceValidator $serviceValidator;
4447

4548

4649
/**
@@ -63,6 +66,8 @@ public function __construct(
6366
// argument in order to facilitate testin.
6467
$this->casConfig = ($casConfig === null || $casConfig === $sspConfig)
6568
? Configuration::getConfig('module_casserver.php') : $casConfig;
69+
$this->serviceValidator = new ServiceValidator($this->casConfig);
70+
6671
$this->authSource = $source ?? new Simple($this->casConfig->getValue('authsource'));
6772
$this->httpUtils = $httpUtils ?? new Utils\HTTP();
6873

@@ -82,49 +87,68 @@ public function __construct(
8287
* @param \Symfony\Component\HttpFoundation\Request $request
8388
* @param string|null $url
8489
*
85-
* @return \SimpleSAML\HTTP\RunnableResponse
90+
* @return \SimpleSAML\XHTML\Template|\SimpleSAML\HTTP\RunnableResponse
8691
*/
8792
public function logout(
8893
Request $request,
8994
#[MapQueryParameter] ?string $url = null,
90-
): RunnableResponse {
95+
#[MapQueryParameter] ?string $service = null,
96+
): Template|RunnableResponse {
9197
if (!$this->casConfig->getOptionalValue('enable_logout', false)) {
9298
$this->handleExceptionThrown('Logout not allowed');
9399
}
94100

95-
// Skip Logout Page configuration
96-
$skipLogoutPage = $this->casConfig->getOptionalValue('skip_logout_page', false);
97-
98-
if ($skipLogoutPage && $url === null) {
99-
$this->handleExceptionThrown('Required URL query parameter [url] not provided. (CAS Server)');
101+
// note: casv3 says to ignore the casv2 url parameter, however deployments will see a mix of cas v2 and
102+
// cas v3 clients so we support both. casv3 makes a query parameter optional
103+
$isCasV3 = empty($url);
104+
$url = $isCasV3 ? $service : $url;
105+
106+
// Validate the return $url is valid
107+
if (!is_null($url)) {
108+
$isValidReturnUrl = !is_null($this->serviceValidator->checkServiceURL($this->sanitize($url)));
109+
if (!$isValidReturnUrl) {
110+
try {
111+
$url = $this->httpUtils->checkURLAllowed($url);
112+
$isValidReturnUrl = true;
113+
} catch (\Exception $e) {
114+
Logger::info('Invalid cas logout url ' . $e->getMessage());
115+
$isValidReturnUrl = false;
116+
}
117+
}
118+
if (!$isValidReturnUrl) {
119+
// Protocol does not define behavior if invalid logout url sent
120+
// act like no url sent and show logout page
121+
Logger::info("Invalid logout url '$url'. Ignoring");
122+
$url = null;
123+
}
100124
}
101125

102-
// Construct the logout redirect url
103-
if ($skipLogoutPage) {
104-
$logoutRedirectUrl = $url;
105-
$params = [];
106-
} else {
107-
$logoutRedirectUrl = Module::getModuleURL('casserver/loggedOut');
108-
$params = $url === null ? []
109-
: ['url' => $url];
110-
}
126+
// Skip Logout Page configuration
127+
$skipLogoutPage = !is_null($url) && ($isCasV3 || $this->casConfig->getOptionalValue('skip_logout_page', false));
128+
111129

112130
// Delete the ticket from the session
113131
$session = $this->getSession();
114132
if ($session !== null) {
115133
$this->ticketStore->deleteTicket($session->getSessionId());
116134
}
117135

118-
// Redirect
119-
if (!$this->authSource->isAuthenticated()) {
120-
return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$logoutRedirectUrl, $params]);
136+
if ($this->authSource->isAuthenticated()) {
137+
// Logout and come back here to handle the logout
138+
return new RunnableResponse(
139+
[$this->authSource, 'logout'],
140+
[$this->httpUtils->getSelfURL()],
141+
);
142+
} elseif ($skipLogoutPage) {
143+
$params = [];
144+
return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, $params]);
145+
} else {
146+
$t = new Template($this->sspConfig, 'casserver:loggedOut.twig');
147+
if ($url) {
148+
$t->data['url'] = $url;
149+
}
150+
return $t;
121151
}
122-
123-
// Logout and redirect
124-
return new RunnableResponse(
125-
[$this->authSource, 'logout'],
126-
[$logoutRedirectUrl],
127-
);
128152
}
129153

130154
/**

0 commit comments

Comments
 (0)