Skip to content
Draft
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
32 changes: 31 additions & 1 deletion components/ILIAS/Init/classes/class.ilStartUpGUI.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
);

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

use Jumbojett\OpenIDConnectClient;
use Jumbojett\OpenIDConnectClientException;

class ilOpenIdConnectClient extends OpenIDConnectClient
{
/**
* @throws OpenIDConnectClientException
*/
public function signOutWithState(string $id_token, string $redirect, string $state): void
{
$sign_out_endpoint = $this->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);
}
}
Loading