roundcube/roundcubemail · warning · RuntimeException
OIDC: Handle only logout events
Error message
OIDC: Handle only logout events
What it means
The OIDC back-channel logout endpoint validates the JWT 'typ' header. Per the OpenID Connect Back-Channel Logout spec the token should be typed 'logout+jwt'; while untyped tokens are tolerated, a token typed as anything else (e.g. 'at+jwt' or an ID token) is rejected because only logout events should reach this endpoint.
Solutions
- Configure the identity provider's back-channel logout to emit tokens with header typ 'logout+jwt'.
- Verify you are pointing the provider's back-channel logout URI at program/actions/login/oauth_backchannel.php and not another endpoint.
- If the provider cannot set 'typ', check its token payload follows the logout-token spec (contains events/sub, no nonce).
- Re-test the endpoint with a spec-compliant logout token (e.g. curl with a signed JWT).
Example fix
// before: header typ 'JWT' or 'at+jwt'
{"typ":"JWT","alg":"RS256"}
// after
{"typ":"logout+jwt","alg":"RS256"} Defensive patterns
Strategy: validation
Validate before calling
$parts = explode('.', $jwt); $header = json_decode(base64_decode(strtr($parts[0], '-_', '+/')), true);
if (!empty($header['typ']) && $header['typ'] !== 'logout+jwt') { /* reject before calling the endpoint */ } Type guard
function isLogoutToken(array $header): bool { return empty($header['typ']) || $header['typ'] === 'logout+jwt'; } Try / catch
try { $handler->run(); } catch (\RuntimeException $e) { http_response_code(400); echo $e->getMessage(); } Prevention
- Configure the IDP's back-channel logout to sign tokens with typ 'logout+jwt'.
- Test the back-channel endpoint with a spec-compliant sample token.
- Point only the back-channel logout URI at this handler, never token endpoints.
When it happens
Trigger: run() receives a back-channel POST whose decoded JWT has a non-empty 'typ' header claim that is not exactly 'logout+jwt'.
Common situations: Identity provider misconfigured to send ID tokens or access tokens instead of proper logout tokens to the back-channel URI; provider not implementing the back-channel logout spec's 'typ' recommendation; testing endpoint manually with a random JWT.
Related errors
- OIDC: event has no "sub"
- OIDC: event has non-empty "nonce"
- Failed to validate JWT: invalid azp value
- Failed to validate JWT: invalid aud value
- Failed to validate JWT: missing aud/azp value
AI-assisted analysis of roundcube/roundcubemail@4b54c2acfb (2026-09-14).
Data as JSON: /api/errors/02532f0de51d7ed0.
Report an issue: GitHub.
Appendix: source
Thrown at program/actions/login/oauth_backchannel.php:63
/* return event example
{
"typ":"Logout", // event type
"iat":1700263584, // emition date
"jti":"4a953d6e-dc6b-4cc1-8d29-cb54b2351d0a", // token identifier
"iss":"https://....", // issuer identifier
"aud":"my client id", // audience = client id
"sub":"82c8f487-df95-4960-972c-4e680c3c72f5", // subject
"sid":"28101815-0017-4ade-a550-e054bde07ded", // session
"events":{"http://schemas.openid.net/event/backchannel-logout":[]}
}
*/
// Validation: https://openid.net/specs/openid-connect-backchannel-1_0.html#rfc.section.2.6
// Note: 'typ' is recommended, not required, so we allow untyped tokens
if (!empty($event['header']['typ']) && $event['header']['typ'] !== 'logout+jwt') {
throw new \RuntimeException('OIDC: Handle only logout events');
}
if (!isset($event['sub'])) {
throw new \RuntimeException('OIDC: event has no "sub"');
}
if (isset($event['nonce'])) {
throw new \RuntimeException('OIDC: event has non-empty "nonce"');
}
$rcmail->oauth->log_debug('backchannel: logout event received, schedule a revocation for token\'s sub: %s', $event['sub']);
$rcmail->oauth->schedule_token_revocation($event['sub']);
http_response_code(200); // 204 works also
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store');
echo '{}';
exit;
} catch (\Exception $e) {
rcube::raise_error($e, true);View on GitHub (pinned to 4b54c2acfb)