From e567ba440837e047b04b8347e01f63f26e189a5c Mon Sep 17 00:00:00 2001 From: mjansen Date: Thu, 16 Jul 2026 10:43:07 +0200 Subject: [PATCH] [FIX] OIDC: Comply with RP-Initiated Logout 1.0 on OpenID Connect global logout This commit fixes global OpenID Connect logout so it complies with [OpenID Connect RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout) Previously, ILIAS sent a dynamic `post_logout_redirect_uri` built via `ilStartUpGUI::logoutUrl()`, including CSRF `rtoken` and user-specific `lang` parameters. According to the specification, this URI **must exactly match** a value previously registered with the OpenID Provider. Strict OPs therefore rejected the logout request. The fix sends a **static, registerable** redirect URI (`/openidconnect.php`, same endpoint as login) and moves dynamic logout context into the ILIAS session. The optional `state` parameter is used as defined by the spec to correlate the OP callback with the pending logout. See: https://mantis.ilias.de/view.php?id=48027 --- .../ILIAS/Init/classes/class.ilStartUpGUI.php | 32 ++++++- .../class.ilAuthProviderOpenIdConnect.php | 83 +++++++++++++++---- .../class.ilOpenIdConnectAppEventListener.php | 6 +- .../classes/class.ilOpenIdConnectClient.php | 44 ++++++++++ 4 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectClient.php diff --git a/components/ILIAS/Init/classes/class.ilStartUpGUI.php b/components/ILIAS/Init/classes/class.ilStartUpGUI.php index 897402770ae1..857803546adc 100755 --- a/components/ILIAS/Init/classes/class.ilStartUpGUI.php +++ b/components/ILIAS/Init/classes/class.ilStartUpGUI.php @@ -1336,7 +1336,8 @@ private function doLogout(): void 'components/ILIAS/Authentication', 'beforeLogout', [ - 'user_id' => $this->user->getId() + 'user_id' => $this->user->getId(), + 'object' => $this->user ] ); @@ -1785,6 +1786,35 @@ private function showOpenIdConnectLoginForm(string $page_editor_html): string private function doOpenIdConnectAuthentication(): void { + $provider = new ilAuthProviderOpenIdConnect(new ilAuthFrontendCredentials()); + if ($provider->isPostLogoutPending()) { + $this->getLogger()->debug('Processing OpenID Connect post-logout callback'); + + $state = $this->http->wrapper()->query()->retrieve( + 'state', + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]) + ); + + if ($state === '' || !$provider->validatePostLogoutState($state)) { + $this->getLogger()->warning('Rejected OpenID Connect post-logout callback due to invalid state.'); + $this->mainTemplate->setOnScreenMessage( + 'failure', + $this->lng->txt('auth_oidc_post_logout_failed'), + true + ); + $this->showLoginPage(); + return; + } + + $this->user->setLanguage($provider->consumePostLogoutUserLanguage()); + + $this->doLogout(); + return; + } + $this->getLogger()->debug('Trying openid connect authentication'); $credentials = new ilAuthFrontendCredentialsOpenIdConnect(); diff --git a/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php b/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php index 59400714c05f..0ab4c34bcb94 100755 --- a/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php +++ b/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php @@ -18,17 +18,16 @@ declare(strict_types=1); -use Jumbojett\OpenIDConnectClient; - class ilAuthProviderOpenIdConnect extends ilAuthProvider { private const OIDC_AUTH_IDTOKEN = 'oidc_auth_idtoken'; + private const OIDC_LOGOUT_STATE = 'oidc_logout_state'; + private const OIDC_LOGOUT_USER_LANGUAGE = 'oidc_logout_user_language'; private const ERR_AUTH_FAILED = 'auth_oidc_failed'; private const ERR_AUTH_WRONG_LOGIN = 'err_wrong_login'; private readonly ilOpenIdConnectSettings $settings; - /** @var array $body */ private readonly ilLogger $logger; private readonly ilLanguage $lng; @@ -43,27 +42,77 @@ public function __construct(ilAuthCredentials $credentials) $this->lng->loadLanguageModule('auth'); } - public function handleLogout(): void + /** + * Static post-logout redirect URI for OP registration (OpenID Connect RP-Initiated Logout). + * Same endpoint as login; pending logout is detected via ilSession. + */ + public function getPostLogoutRedirectUri(): string + { + return ILIAS_HTTP_PATH . '/openidconnect.php'; + } + + public function isPostLogoutPending(): bool + { + $state = ilSession::get(self::OIDC_LOGOUT_STATE); + + return is_string($state) && $state !== ''; + } + + public function handleLogout(ilObjUser $user): void { if ($this->settings->getLogoutScope() === ilOpenIdConnectSettings::LOGOUT_SCOPE_LOCAL) { return; } $id_token = ilSession::get(self::OIDC_AUTH_IDTOKEN); + if (!isset($id_token) || $id_token === '') { + return; + } + $this->logger->debug('Logging out with token: ' . $id_token); - if (isset($id_token) && $id_token !== '') { - ilSession::set(self::OIDC_AUTH_IDTOKEN, ''); - $oidc = $this->initClient(); - try { - $oidc->signOut( - $id_token, - ILIAS_HTTP_PATH . '/' . ilStartUpGUI::logoutUrl() - ); - } catch (\Jumbojett\OpenIDConnectClientException $e) { - $this->logger->warning('Logging out of OIDC provider failed with: ' . $e->getMessage()); - } + // Keep dynamic logout context in the ILIAS session; only a static URI is sent to the OP. + $state = bin2hex(random_bytes(16)); + ilSession::set(self::OIDC_LOGOUT_STATE, $state); + ilSession::set(self::OIDC_LOGOUT_USER_LANGUAGE, $user->getLanguage()); + ilSession::set(self::OIDC_AUTH_IDTOKEN, ''); + + $oidc = $this->initClient(); + + try { + $oidc->signOutWithState( + $id_token, + $this->getPostLogoutRedirectUri(), + $state + ); + } catch (\Jumbojett\OpenIDConnectClientException $e) { + ilSession::set(self::OIDC_LOGOUT_STATE, ''); + ilSession::set(self::OIDC_LOGOUT_USER_LANGUAGE, ''); + $this->logger->warning('Logging out of OIDC provider failed with: ' . $e->getMessage()); + } + } + + public function validatePostLogoutState(string $state): bool + { + $expected_state = ilSession::get(self::OIDC_LOGOUT_STATE); + + if (!is_string($expected_state) || $expected_state === '' || !hash_equals($expected_state, $state)) { + $this->logger->warning('OpenID Connect post-logout state validation failed.'); + + return false; } + + ilSession::set(self::OIDC_LOGOUT_STATE, ''); + + return true; + } + + public function consumePostLogoutUserLanguage(): string + { + $language = ilSession::get(self::OIDC_LOGOUT_USER_LANGUAGE); + ilSession::set(self::OIDC_LOGOUT_USER_LANGUAGE, ''); + + return is_string($language) && $language !== '' ? $language : 'en'; } public function doAuthentication(ilAuthStatus $status): bool @@ -172,9 +221,9 @@ private function handleUpdate(ilAuthStatus $status, $user_info): ilAuthStatus return $status; } - private function initClient(): OpenIDConnectClient + private function initClient(): ilOpenIdConnectClient { - $oidc = new OpenIDConnectClient( + $oidc = new ilOpenIdConnectClient( $this->settings->getProvider(), $this->settings->getClientId(), $this->settings->getSecret() diff --git a/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectAppEventListener.php b/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectAppEventListener.php index 8b3ef7911601..2bb00db853bb 100755 --- a/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectAppEventListener.php +++ b/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectAppEventListener.php @@ -20,10 +20,10 @@ class ilOpenIdConnectAppEventListener implements ilAppEventListener { - protected function handleLogoutFor(int $user_id): void + protected function handleLogoutFor(ilObjUser $user): void { $provider = new ilAuthProviderOpenIdConnect(new ilAuthFrontendCredentials()); - $provider->handleLogout(); + $provider->handleLogout($user); } public static function handleEvent(string $a_component, string $a_event, array $a_parameter): void @@ -34,7 +34,7 @@ public static function handleEvent(string $a_component, string $a_event, array $ if (($a_component === 'components/ILIAS/Authentication') && $a_event === 'beforeLogout') { $listener = new self(); - $listener->handleLogoutFor($a_parameter['user_id']); + $listener->handleLogoutFor($a_parameter['object']); } } } diff --git a/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectClient.php b/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectClient.php new file mode 100644 index 000000000000..66b050b13c91 --- /dev/null +++ b/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectClient.php @@ -0,0 +1,44 @@ +getProviderConfigValue('end_session_endpoint'); + + $signout_params = [ + 'id_token_hint' => $id_token, + 'post_logout_redirect_uri' => $redirect, + 'state' => $state, + ]; + + $sign_out_endpoint .= (!str_contains($sign_out_endpoint, '?') ? '?' : '&') + . http_build_query($signout_params, '', '&', $this->encType); + + $this->redirect($sign_out_endpoint); + } +}