feat(oidc): pin at+jwt audience on the Resource-Server verifier path #273

Merged
YousifShkara merged 1 commit from fix/BUNYIP-252-at-jwt-audience-binding into main 2026-06-30 04:56:04 +02:00
Owner

BUNYIP-252: verify_at_jwt_claims deliberately sets validate_aud = false because the OIDC userinfo endpoint MUST accept any at+jwt this OP issued (OIDC Core §5.3). The same verifier is also reused by the AtJwtVerifier impl that protects bunyip-API's /v1/* routes, which makes every bunyip-API surface a confused deputy: a mokosh / drillmark / lets-chat at+jwt presented as a Bearer credential to /v1/membership, /v1/billing, /v1/admin/* is accepted as the token's sub. RFC 9068 §4 explicitly says a Resource Server MUST refuse a token whose aud does not name it.

Split the verifier: verify_at_jwt_claims keeps its permissive shape (userinfo's spec-mandated behaviour), and a new verify_at_jwt_for_rs pins validation.set_audience(&[OidcConfig::rs_audience]). The AtJwtVerifier::verify_and_resolve impl routes through the strict variant, so the /v1/* family now rejects RP-issued at+jwts.

OidcConfig::rs_audience defaults to urn:bunyip:rs and is overridable via OIDC_RS_AUDIENCE. Future Bearer-at+jwt callers register a client whose audience is the configured value and acquire tokens via the normal authorize/token dance; the current consumers of /v1/* (bunyip-web BFF via legacy HS256 cookie + the OIDC userinfo endpoint) are unaffected.

The end-to-end mint/verify test belongs in a separate test-harness ticket: bunyip-oidc has no integration harness today and adding one is heavier than the audience-bind fix this ticket is about. The wiring is straightforward (one set_audience call, called only from the RS path) and is exercised by every existing handler test that already touches AtJwtVerifier.

#BUNYIP-252

BUNYIP-252: `verify_at_jwt_claims` deliberately sets `validate_aud = false` because the OIDC userinfo endpoint MUST accept any at+jwt this OP issued (OIDC Core §5.3). The same verifier is also reused by the `AtJwtVerifier` impl that protects bunyip-API's `/v1/*` routes, which makes every bunyip-API surface a confused deputy: a mokosh / drillmark / lets-chat at+jwt presented as a Bearer credential to `/v1/membership`, `/v1/billing`, `/v1/admin/*` is accepted as the token's `sub`. RFC 9068 §4 explicitly says a Resource Server MUST refuse a token whose `aud` does not name it. Split the verifier: `verify_at_jwt_claims` keeps its permissive shape (userinfo's spec-mandated behaviour), and a new `verify_at_jwt_for_rs` pins `validation.set_audience(&[OidcConfig::rs_audience])`. The `AtJwtVerifier::verify_and_resolve` impl routes through the strict variant, so the `/v1/*` family now rejects RP-issued at+jwts. `OidcConfig::rs_audience` defaults to `urn:bunyip:rs` and is overridable via `OIDC_RS_AUDIENCE`. Future Bearer-at+jwt callers register a client whose audience is the configured value and acquire tokens via the normal authorize/token dance; the current consumers of `/v1/*` (bunyip-web BFF via legacy HS256 cookie + the OIDC userinfo endpoint) are unaffected. The end-to-end mint/verify test belongs in a separate test-harness ticket: bunyip-oidc has no integration harness today and adding one is heavier than the audience-bind fix this ticket is about. The wiring is straightforward (one `set_audience` call, called only from the RS path) and is exercised by every existing handler test that already touches `AtJwtVerifier`. #BUNYIP-252
feat(oidc): pin at+jwt audience on the Resource-Server verifier path
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 49s
Check / fmt + clippy + build + tests (pull_request) Successful in 27m2s
Create release / Create release from merged PR (pull_request) Has been skipped
9203617f2f
BUNYIP-252: `verify_at_jwt_claims` deliberately sets `validate_aud = false` because the OIDC userinfo endpoint MUST accept any at+jwt this OP issued (OIDC Core §5.3). The same verifier is also reused by the `AtJwtVerifier` impl that protects bunyip-API's `/v1/*` routes, which makes every bunyip-API surface a confused deputy: a mokosh / drillmark / lets-chat at+jwt presented as a Bearer credential to `/v1/membership`, `/v1/billing`, `/v1/admin/*` is accepted as the token's `sub`. RFC 9068 §4 explicitly says a Resource Server MUST refuse a token whose `aud` does not name it.

Split the verifier: `verify_at_jwt_claims` keeps its permissive shape (userinfo's spec-mandated behaviour), and a new `verify_at_jwt_for_rs` pins `validation.set_audience(&[OidcConfig::rs_audience])`. The `AtJwtVerifier::verify_and_resolve` impl routes through the strict variant, so the `/v1/*` family now rejects RP-issued at+jwts.

`OidcConfig::rs_audience` defaults to `urn:bunyip:rs` and is overridable via `OIDC_RS_AUDIENCE`. Future Bearer-at+jwt callers register a client whose audience is the configured value and acquire tokens via the normal authorize/token dance; the current consumers of `/v1/*` (bunyip-web BFF via legacy HS256 cookie + the OIDC userinfo endpoint) are unaffected.

The end-to-end mint/verify test belongs in a separate test-harness ticket: bunyip-oidc has no integration harness today and adding one is heavier than the audience-bind fix this ticket is about. The wiring is straightforward (one `set_audience` call, called only from the RS path) and is exercised by every existing handler test that already touches `AtJwtVerifier`.

#BUNYIP-252
YousifShkara deleted branch fix/BUNYIP-252-at-jwt-audience-binding 2026-06-30 04:56:05 +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!273
No description provided.