fix(oidc): scope /oauth2/logout post_logout_redirect_uri to the id_token_hint's client #279

Merged
YousifShkara merged 1 commit from fix/BUNYIP-260-logout-id-token-hint-compliance into main 2026-06-30 05:59:50 +02:00
Owner

BUNYIP-260: close the cross-RP open-redirect surface on /oauth2/logout.

LogoutQuery previously accepted post_logout_redirect_uri + state with no identifier of the calling client. The runtime looked up the URI against EVERY oauth_clients row's post_logout_redirect_uris allowlist, so RP-A could craft a logout URL pointing at RP-A's registered redirect, bounce a victim mid-logout with attacker-chosen state while the victim thought they were leaving RP-B.

OpenID Connect RP-Initiated Logout 1.0 §3 fixes this with id_token_hint: the calling RP presents the id_token the OP issued to it, the OP identifies the client from aud, and ONLY that client's allowlist is consulted. Add the parameter to LogoutQuery, decode the hint via a new OidcProvider::verify_id_token_client helper (signature + issuer + known kid, exp deliberately NOT checked because RPs commonly hand in expired hints), and narrow the SQL lookup to client_id = $1 AND disabled_at IS NULL AND $2 = ANY(post_logout_redirect_uris).

Backwards compatibility: bare /oauth2/logout (no redirect, no hint) keeps working as a "just sign me out" call - the request still revokes sessions, fires back-channel logout, and lands on "/". The only behaviour change is: if a post_logout_redirect_uri is supplied WITHOUT a verifiable id_token_hint, the redirect is refused and the user lands on "/" instead (logged at warn for operator visibility). RPs that want the redirect must pass the hint - exactly the spec-compliant shape.

The audit also called out CSRF on GET /oauth2/logout (<img src> revokes a victim's session); that's deliberately deferred. The hint-binding change collapses the OPEN-REDIRECT surface here without adding a POST requirement that would break every legitimate RP integration. A follow-up ticket can take the POST+CSRF question separately.

#BUNYIP-260

BUNYIP-260: close the cross-RP open-redirect surface on `/oauth2/logout`. `LogoutQuery` previously accepted `post_logout_redirect_uri` + `state` with no identifier of the calling client. The runtime looked up the URI against EVERY oauth_clients row's `post_logout_redirect_uris` allowlist, so RP-A could craft a logout URL pointing at RP-A's registered redirect, bounce a victim mid-logout with attacker-chosen `state` while the victim thought they were leaving RP-B. OpenID Connect RP-Initiated Logout 1.0 §3 fixes this with `id_token_hint`: the calling RP presents the id_token the OP issued to it, the OP identifies the client from `aud`, and ONLY that client's allowlist is consulted. Add the parameter to `LogoutQuery`, decode the hint via a new `OidcProvider::verify_id_token_client` helper (signature + issuer + known kid, `exp` deliberately NOT checked because RPs commonly hand in expired hints), and narrow the SQL lookup to `client_id = $1 AND disabled_at IS NULL AND $2 = ANY(post_logout_redirect_uris)`. Backwards compatibility: bare `/oauth2/logout` (no redirect, no hint) keeps working as a "just sign me out" call - the request still revokes sessions, fires back-channel logout, and lands on "/". The only behaviour change is: if a `post_logout_redirect_uri` is supplied WITHOUT a verifiable `id_token_hint`, the redirect is refused and the user lands on "/" instead (logged at warn for operator visibility). RPs that want the redirect must pass the hint - exactly the spec-compliant shape. The audit also called out CSRF on GET /oauth2/logout (`<img src>` revokes a victim's session); that's deliberately deferred. The hint-binding change collapses the OPEN-REDIRECT surface here without adding a POST requirement that would break every legitimate RP integration. A follow-up ticket can take the POST+CSRF question separately. #BUNYIP-260
fix(oidc): scope /oauth2/logout post_logout_redirect_uri to the id_token_hint's client
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 22s
Check / fmt + clippy + build + tests (pull_request) Successful in 11m3s
Create release / Create release from merged PR (pull_request) Has been skipped
3f44c35961
BUNYIP-260: close the cross-RP open-redirect surface on `/oauth2/logout`.

`LogoutQuery` previously accepted `post_logout_redirect_uri` + `state` with no identifier of the calling client. The runtime looked up the URI against EVERY oauth_clients row's `post_logout_redirect_uris` allowlist, so RP-A could craft a logout URL pointing at RP-A's registered redirect, bounce a victim mid-logout with attacker-chosen `state` while the victim thought they were leaving RP-B.

OpenID Connect RP-Initiated Logout 1.0 §3 fixes this with `id_token_hint`: the calling RP presents the id_token the OP issued to it, the OP identifies the client from `aud`, and ONLY that client's allowlist is consulted. Add the parameter to `LogoutQuery`, decode the hint via a new `OidcProvider::verify_id_token_client` helper (signature + issuer + known kid, `exp` deliberately NOT checked because RPs commonly hand in expired hints), and narrow the SQL lookup to `client_id = $1 AND disabled_at IS NULL AND $2 = ANY(post_logout_redirect_uris)`.

Backwards compatibility: bare `/oauth2/logout` (no redirect, no hint) keeps working as a "just sign me out" call - the request still revokes sessions, fires back-channel logout, and lands on "/". The only behaviour change is: if a `post_logout_redirect_uri` is supplied WITHOUT a verifiable `id_token_hint`, the redirect is refused and the user lands on "/" instead (logged at warn for operator visibility). RPs that want the redirect must pass the hint - exactly the spec-compliant shape.

The audit also called out CSRF on GET /oauth2/logout (`<img src>` revokes a victim's session); that's deliberately deferred. The hint-binding change collapses the OPEN-REDIRECT surface here without adding a POST requirement that would break every legitimate RP integration. A follow-up ticket can take the POST+CSRF question separately.

#BUNYIP-260
YousifShkara deleted branch fix/BUNYIP-260-logout-id-token-hint-compliance 2026-06-30 05:59:50 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!279
No description provided.