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

  1. Configure the IDP to include the 'sub' claim in back-channel logout tokens.
  2. If your provider only supports 'sid', extend the handler to resolve sessions by 'sid' instead of 'sub'.
  3. Decode the received JWT (e.g. jwt.io) to confirm which claims are present.
  4. 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

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


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)