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

  1. Configure the identity provider's back-channel logout to emit tokens with header typ 'logout+jwt'.
  2. Verify you are pointing the provider's back-channel logout URI at program/actions/login/oauth_backchannel.php and not another endpoint.
  3. If the provider cannot set 'typ', check its token payload follows the logout-token spec (contains events/sub, no nonce).
  4. 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

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


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)