{"record":{"id":"7957396b9073f96e","repo":"roundcube/roundcubemail","slug":"oidc-event-has-no-sub","errorCode":null,"errorMessage":"OIDC: event has no \"sub\"","messagePattern":"OIDC: event has no \"sub\"","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"warning","filePath":"program/actions/login/oauth_backchannel.php","lineNumber":66,"sourceCode":"                    \"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);\n                $answer['error_description'] = 'Error decoding JWT';\n            }\n        } else {","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/roundcube/roundcubemail/blob/4b54c2acfb54d5ee3d1c281ca7f143bed0dea804/program/actions/login/oauth_backchannel.php#L48-L84","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: payload missing sub\n{\"iss\":\"https://idp\",\"aud\":\"rc\",\"events\":{\"http://schemas.openid.net/event/backchannel-logout\":{}}}\n// after\n{\"iss\":\"https://idp\",\"aud\":\"rc\",\"sub\":\"user123\",\"events\":{\"http://schemas.openid.net/event/backchannel-logout\":{}}}","handlingStrategy":"validation","validationCode":"$body = json_decode(base64_decode(strtr(explode('.', $jwt)[1], '-_', '+/')), true);\nif (!isset($body['sub'])) { /* reject: logout token without subject */ }","typeGuard":"function hasSubject(array $claims): bool { return isset($claims['sub']) && is_string($claims['sub']); }","tryCatchPattern":"try { $handler->run(); } catch (\\RuntimeException $e) { http_response_code(400); error_log('backchannel: ' . $e->getMessage()); }","preventionTips":["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."],"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"}