{"record":{"id":"323a4fd229c6c6fd","repo":"roundcube/roundcubemail","slug":"oidc-event-has-non-empty-nonce","errorCode":null,"errorMessage":"OIDC: event has non-empty \"nonce\"","messagePattern":"OIDC: event has non-empty \"nonce\"","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"warning","filePath":"program/actions/login/oauth_backchannel.php","lineNumber":69,"sourceCode":"                    \"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 {\n            rcube::raise_error(sprintf('oidc backchannel called from %s without any parameter', rcube_utils::remote_addr()), true);\n        }\n","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/roundcube/roundcubemail/blob/4b54c2acfb54d5ee3d1c281ca7f143bed0dea804/program/actions/login/oauth_backchannel.php#L51-L87","documentation":"Per the OIDC Back-Channel Logout spec, a logout token MUST NOT contain a 'nonce' claim (that would make it replayable as an ID token). The handler rejects any token carrying a nonce.","triggerScenarios":"run() receives a JWT whose payload includes a 'nonce' key — typically an ID token was sent to the back-channel logout endpoint instead of a proper logout token.","commonSituations":"Misconfigured IDP wiring the token/ID-token endpoint output into back-channel logout; custom middleware forwarding access tokens; developer testing with an ID token copied from a login flow.","solutions":["Fix the IDP so it emits a real logout token (no nonce, has 'events' claim) to the back-channel URI.","Confirm the back-channel logout URI is set to the oauth_backchannel action, not the login/token endpoints.","Inspect the incoming JWT payload and remove 'nonce' if generating tokens in a test harness.","Update the IDP version if it has a known bug including nonce in logout tokens."],"exampleFix":"// before (ID token sent as logout token)\n{\"iss\":\"https://idp\",\"aud\":\"rc\",\"sub\":\"u1\",\"nonce\":\"abc123\"}\n// after\n{\"iss\":\"https://idp\",\"aud\":\"rc\",\"sub\":\"u1\",\"events\":{\"http://schemas.openid.net/event/backchannel-logout\":{}}}","handlingStrategy":"validation","validationCode":"$body = json_decode(base64_decode(strtr(explode('.', $jwt)[1], '-_', '+/')), true);\nif (isset($body['nonce'])) { /* reject: nonce present, not a logout token */ }","typeGuard":"function isNonceFree(array $claims): bool { return !array_key_exists('nonce', $claims); }","tryCatchPattern":"try { $handler->run(); } catch (\\RuntimeException $e) { http_response_code(400); error_log('backchannel: ' . $e->getMessage()); }","preventionTips":["Never wire ID-token issuance into the back-channel logout URI.","Verify logout tokens contain 'events' and 'sub' but no 'nonce'.","Test with the IDP's documented logout-token example."],"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"}