Skip to content

Commit 158c408

Browse files
committed
fix: provider logout redirect and robust direct admin action routing
1 parent 4f1deda commit 158c408

2 files changed

Lines changed: 74 additions & 7 deletions

File tree

plugins/universal_oidc_mail_sso/lib/OidcClient.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,27 @@ public function fetchUserinfo(array $discovery, string $accessToken): array
126126
]);
127127
}
128128

129+
public function buildEndSessionUrl(array $discovery, string $idTokenHint, string $postLogoutRedirectUri = ''): string
130+
{
131+
$endpoint = (string) ($discovery['end_session_endpoint'] ?? '');
132+
if ($endpoint === '') {
133+
return '';
134+
}
135+
136+
$params = [
137+
'id_token_hint' => $idTokenHint,
138+
];
139+
140+
if ($postLogoutRedirectUri !== '') {
141+
$params['post_logout_redirect_uri'] = $postLogoutRedirectUri;
142+
if (!empty($this->config['client_id'])) {
143+
$params['client_id'] = (string) $this->config['client_id'];
144+
}
145+
}
146+
147+
return $endpoint . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
148+
}
149+
129150
private function httpJson(string $method, string $url, array $options = []): array
130151
{
131152
$body = $this->http($method, $url, $options);

plugins/universal_oidc_mail_sso/universal_oidc_mail_sso.php

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313

1414
class universal_oidc_mail_sso extends rcube_plugin
1515
{
16-
public $task = 'login|mail|settings';
16+
public $task = 'login|mail|settings|logout';
1717

1818
private const SESSION_OIDC_SUB = 'universal_oidc_mail_sso_oidc_sub';
1919
private const SESSION_OIDC_EMAIL = 'universal_oidc_mail_sso_oidc_email';
2020
private const SESSION_OIDC_NONCE = 'universal_oidc_mail_sso_oidc_nonce';
2121
private const SESSION_OIDC_STATE = 'universal_oidc_mail_sso_oidc_state';
2222
private const SESSION_OIDC_CODE_VERIFIER = 'universal_oidc_mail_sso_oidc_code_verifier';
2323
private const SESSION_OIDC_GROUPS = 'universal_oidc_mail_sso_oidc_groups';
24+
private const SESSION_OIDC_ID_TOKEN = 'universal_oidc_mail_sso_id_token';
2425
private const SESSION_AUTLOGIN = 'universal_oidc_mail_sso_autologin';
2526
private const SESSION_SETUP_RATE = 'universal_oidc_mail_sso_setup_rate';
2627
private const PREF_OIDC_GROUPS = 'universal_oidc_mail_sso_groups';
@@ -39,6 +40,7 @@ class universal_oidc_mail_sso extends rcube_plugin
3940
private ?Crypto $crypto = null;
4041
private ?string $cryptoInitError = null;
4142
private ?array $policyCache = null;
43+
private ?string $pendingLogoutIdToken = null;
4244

4345
public function init(): void
4446
{
@@ -57,6 +59,7 @@ public function init(): void
5759
$this->add_hook('authenticate', [$this, 'authenticate']);
5860
$this->add_hook('storage_connect', [$this, 'storageConnect']);
5961
$this->add_hook('smtp_connect', [$this, 'smtpConnect']);
62+
$this->add_hook('logout_after', [$this, 'logoutAfter']);
6063

6164
$this->register_action(self::ACTION_LOGIN, [$this, 'actionLogin']);
6265
$this->register_action(self::ACTION_CALLBACK, [$this, 'actionCallback']);
@@ -72,22 +75,43 @@ public function init(): void
7275
public function startup(array $args): array
7376
{
7477
$action = rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC);
78+
$adminActions = [
79+
self::ACTION_ADMIN,
80+
self::ACTION_ADMIN_SAVE_POLICY,
81+
self::ACTION_ADMIN_DELETE_USER,
82+
self::ACTION_ADMIN_SET_USER_STATUS,
83+
];
84+
85+
// Ensure direct admin action links always route to a valid auth flow.
86+
if (is_string($action) && in_array($action, $adminActions, true)) {
87+
if (empty($_SESSION['user_id'])) {
88+
$this->redirectTo($this->urlForAction(self::ACTION_LOGIN));
89+
}
90+
91+
if ($this->rc->task !== 'settings') {
92+
$this->redirectTo($this->rc->url(['task' => 'settings', 'action' => $action]));
93+
}
94+
}
7595

7696
// If admin plugin endpoints are accessed directly without a Roundcube
7797
// session, start OIDC login flow instead of failing with access errors.
7898
if ($this->rc->task === 'settings'
7999
&& is_string($action)
80-
&& in_array($action, [
81-
self::ACTION_ADMIN,
82-
self::ACTION_ADMIN_SAVE_POLICY,
83-
self::ACTION_ADMIN_DELETE_USER,
84-
self::ACTION_ADMIN_SET_USER_STATUS,
85-
], true)
100+
&& in_array($action, $adminActions, true)
86101
&& empty($_SESSION['user_id'])
87102
) {
88103
$this->redirectTo($this->urlForAction(self::ACTION_LOGIN));
89104
}
90105

106+
// Capture id_token on logout request. We'll redirect to OIDC
107+
// end_session_endpoint in logoutAfter(), after local logout completes.
108+
if ($this->rc->task === 'logout') {
109+
$idToken = $_SESSION[self::SESSION_OIDC_ID_TOKEN] ?? null;
110+
if (is_string($idToken) && $idToken !== '') {
111+
$this->pendingLogoutIdToken = $idToken;
112+
}
113+
}
114+
91115
if ($this->rc->task !== 'login' || !empty($_SESSION['user_id'])) {
92116
return $args;
93117
}
@@ -319,6 +343,7 @@ public function actionCallback(): void
319343
$_SESSION[self::SESSION_OIDC_SUB] = $sub;
320344
$_SESSION[self::SESSION_OIDC_EMAIL] = $email;
321345
$_SESSION[self::SESSION_OIDC_GROUPS] = $groups;
346+
$_SESSION[self::SESSION_OIDC_ID_TOKEN] = (string) ($token['id_token'] ?? '');
322347

323348
$existingIdentity = $this->storage->getIdentityBySub($sub);
324349
if (!empty($existingIdentity['is_disabled'])) {
@@ -345,6 +370,27 @@ public function actionCallback(): void
345370
$this->redirectTo($this->urlForAction(self::ACTION_AUTOLOGIN));
346371
}
347372

373+
public function logoutAfter(array $args): array
374+
{
375+
if ($this->pendingLogoutIdToken === null || $this->pendingLogoutIdToken === '') {
376+
return $args;
377+
}
378+
379+
try {
380+
$oidc = $this->oidcClient();
381+
$discovery = $oidc->discover();
382+
$postLogoutRedirect = (string) $this->cfg('post_logout_redirect_uri', $this->externalBaseUrl() . '/');
383+
$url = $oidc->buildEndSessionUrl($discovery, $this->pendingLogoutIdToken, $postLogoutRedirect);
384+
if ($url !== '') {
385+
$this->redirectTo($url);
386+
}
387+
} catch (Throwable $e) {
388+
$this->log('oidc_logout_redirect_failed', ['err' => $e->getMessage()]);
389+
}
390+
391+
return $args;
392+
}
393+
348394
public function actionAutologin(): void
349395
{
350396
$oidcSub = $_SESSION[self::SESSION_OIDC_SUB] ?? null;

0 commit comments

Comments
 (0)