{"record":{"id":"02532f0de51d7ed0","repo":"roundcube/roundcubemail","slug":"oidc-handle-only-logout-events","errorCode":null,"errorMessage":"OIDC: Handle only logout events","messagePattern":"OIDC: Handle only logout events","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"warning","filePath":"program/actions/login/oauth_backchannel.php","lineNumber":63,"sourceCode":"\n                /* return event example\n                {\n                    \"typ\":\"Logout\",                                      // event type\n                    \"iat\":1700263584,                                    // emition date\n                    \"jti\":\"4a953d6e-dc6b-4cc1-8d29-cb54b2351d0a\",        // token identifier\n                    \"iss\":\"https://....\",                                // issuer identifier\n                    \"aud\":\"my client id\",                                // audience = client id\n                    \"sub\":\"82c8f487-df95-4960-972c-4e680c3c72f5\",        // subject\n                    \"sid\":\"28101815-0017-4ade-a550-e054bde07ded\",        // session\n                    \"events\":{\"http://schemas.openid.net/event/backchannel-logout\":[]}\n                }\n                */\n\n                // Validation: https://openid.net/specs/openid-connect-backchannel-1_0.html#rfc.section.2.6\n\n                // Note: 'typ' is recommended, not required, so we allow untyped tokens\n                if (!empty($event['header']['typ']) && $event['header']['typ'] !== 'logout+jwt') {\n                    throw new \\RuntimeException('OIDC: Handle only logout events');\n                }\n                if (!isset($event['sub'])) {\n                    throw new \\RuntimeException('OIDC: event has no \"sub\"');\n                }\n                if (isset($event['nonce'])) {\n                    throw new \\RuntimeException('OIDC: event has non-empty \"nonce\"');\n                }\n\n                $rcmail->oauth->log_debug('backchannel: logout event received, schedule a revocation for token\\'s sub: %s', $event['sub']);\n                $rcmail->oauth->schedule_token_revocation($event['sub']);\n\n                http_response_code(200); // 204 works also\n                header('Content-Type: application/json; charset=UTF-8');\n                header('Cache-Control: no-store');\n                echo '{}';\n                exit;\n            } catch (\\Exception $e) {\n                rcube::raise_error($e, true);","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/roundcube/roundcubemail/blob/4b54c2acfb54d5ee3d1c281ca7f143bed0dea804/program/actions/login/oauth_backchannel.php#L45-L81","documentation":"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.","triggerScenarios":"run() receives a back-channel POST whose decoded JWT has a non-empty 'typ' header claim that is not exactly 'logout+jwt'.","commonSituations":"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.","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)."],"exampleFix":"// before: header typ 'JWT' or 'at+jwt'\n{\"typ\":\"JWT\",\"alg\":\"RS256\"}\n// after\n{\"typ\":\"logout+jwt\",\"alg\":\"RS256\"}","handlingStrategy":"validation","validationCode":"$parts = explode('.', $jwt); $header = json_decode(base64_decode(strtr($parts[0], '-_', '+/')), true);\nif (!empty($header['typ']) && $header['typ'] !== 'logout+jwt') { /* reject before calling the endpoint */ }","typeGuard":"function isLogoutToken(array $header): bool { return empty($header['typ']) || $header['typ'] === 'logout+jwt'; }","tryCatchPattern":"try { $handler->run(); } catch (\\RuntimeException $e) { http_response_code(400); echo $e->getMessage(); }","preventionTips":["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."],"tags":["oidc","jwt","logout","validation"],"backgroundTag":"unexpected-response-shape","analyzedSha":"4b54c2acfb54d5ee3d1c281ca7f143bed0dea804","analyzedAt":"2026-09-14T13:23:19.231Z","contentChangedAt":"2026-09-14T13:23:19.231Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}