roundcube/roundcubemail · warning · RuntimeException
OIDC: event has no "sub"
Error message
OIDC: event has no "sub"
What it means
A back-channel logout JWT must contain a 'sub' claim identifying the user whose tokens should be revoked. The handler throws when $event['sub'] is absent, because without a subject it cannot schedule the token revocation.
Solutions
- Configure the IDP to include the 'sub' claim in back-channel logout tokens.
- If your provider only supports 'sid', extend the handler to resolve sessions by 'sid' instead of 'sub'.
- Decode the received JWT (e.g. jwt.io) to confirm which claims are present.
- Ensure the token audience/azp corresponds to the Roundcube client so the full subject is included.
Example fix
// before: payload missing sub
{"iss":"https://idp","aud":"rc","events":{"http://schemas.openid.net/event/backchannel-logout":{}}}
// after
{"iss":"https://idp","aud":"rc","sub":"user123","events":{"http://schemas.openid.net/event/backchannel-logout":{}}} Defensive patterns
Strategy: validation
Validate before calling
$body = json_decode(base64_decode(strtr(explode('.', $jwt)[1], '-_', '+/')), true);
if (!isset($body['sub'])) { /* reject: logout token without subject */ } Type guard
function hasSubject(array $claims): bool { return isset($claims['sub']) && is_string($claims['sub']); } Try / catch
try { $handler->run(); } catch (\RuntimeException $e) { http_response_code(400); error_log('backchannel: ' . $e->getMessage()); } Prevention
- Ensure the IDP includes 'sub' in logout tokens (not only 'sid').
- Decode incoming tokens during initial IDP integration testing.
- If only 'sid' is supported, extend the handler explicitly rather than hoping sub appears.
When it happens
Trigger: run() decodes a logout token whose body lacks the 'sub' claim — the IDP sent a logout event without a subject, or only a 'sid' (session id) claim per the optional part of the spec.
Common situations: Keycloak/Auth0/other IDPs configured for session-id-only logout tokens; token built manually for testing without 'sub'; provider emitting 'logout_token' with 'events' but empty claims set.
Related errors
- OIDC: Handle only logout events
- 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/7957396b9073f96e.
Report an issue: GitHub.
Appendix: source
Thrown at program/actions/login/oauth_backchannel.php:66
"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);
$answer['error_description'] = 'Error decoding JWT';
}
} else {View on GitHub (pinned to 4b54c2acfb)