feat: RFC 9396 RAR token-side (JWT embed, token response, introspection) - #168
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements the token-side wiring for Rich Authorization Requests (RFC 9396) in the CIBA flow. Specifically, when a CIBA request containing authorization_details is approved, the granted payload is now embedded as a top-level claim in the access-token JWT (§6.1), surfaced in the token response body (§5.2), and exposed via token introspection (§7). The implementation ensures that legacy CIBA flows without RAR are not contaminated with empty authorization_details fields. Comprehensive integration and compliance tests have been added to verify these behaviors across all three surfaces. There are no review comments to address, and I have no additional feedback to provide.
|
@saucam I don't think we need legacy preservation, do we? |
There was a problem hiding this comment.
Pull request overview
Implements the token-side surfaces for RFC 9396 Rich Authorization Requests (RAR) in the CIBA flow, propagating granted authorization_details into the access-token JWT, token response body, and introspection response while preserving legacy behavior (field omitted when not granted).
Changes:
- Add
authorization_detailsto the/oauth2/tokensuccess response (poll + push) and embed it as a top-level access-token JWT claim. - Surface
authorization_detailsin/oauth2/token/introspectby adding it to the introspection allow-list. - Extend integration/compliance coverage and update documentation to reflect full RFC 9396 coverage.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/integration/rar_compliance_test.go | Adds RFC 9396 §5.2/§6.1/§7 compliance tests and shared token-issuance helper. |
| tests/integration/COMPLIANCE.md | Marks RFC 9396 compliance as fully covered. |
| tests/integration/ciba_rar_test.go | Adds end-to-end token-side RAR test plus legacy “omit everywhere” regression test. |
| README.md | Updates feature list to reflect token-side RAR support (token response/JWT/introspection). |
| internal/service/oauth.go | Surfaces authorization_details in introspection output by allow-listing the claim. |
| internal/service/backchannel.go | Embeds/passes through persisted RAR into JWT custom claims and token responses; adds helper to omit empty cases. |
| domain/token.go | Adds AccessToken.AuthorizationDetails json.RawMessage with omitempty for token responses. |
| domain/backchannel_auth.go | Updates commentary to reflect token-side embedding now being part of the system. |
| docs/rar.md | Documents token-side wiring surfaces and updates compliance suite description. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
778a9b4 to
10b60d4
Compare
|
Force-pushed a rebase onto latest |
2982d96 to
a7f6847
Compare
|
Agreed, dropped — Pushed in
Net: removed the re-parse on every CIBA token issuance, ~30 LOC of helper, and one branch in three places. Same external shape for RAR-bearing tokens; legacy tokens now also carry |
|
don't think we need legacy preservation, do we? No |
Completes the RFC 9396 implementation begun in #164. When a CIBA request that carried `authorization_details` is approved and a token is issued, the granted typed payload is now surfaced on three coordinated surfaces: - Token response body (§5.2): `authorization_details` field on the /oauth2/token success response. - Access-token JWT claim (§6.1): top-level `authorization_details` claim on the issued JWT — resource servers read the typed grant without an introspection round-trip. - Introspection response (§7): `authorization_details` field on /oauth2/token/introspect — works for resource servers that don't share JWKS. All three surfaces carry the verbatim bytes the client supplied on bc-authorize. zeroid does not modify granted RAR in this release. ## What ships - `domain/token.go` — `AccessToken.AuthorizationDetails json.RawMessage` with `omitempty` so legacy CIBA token responses stay clean. - `internal/service/backchannel.go` — `issueTokenForApprovedRow` reads the persisted RAR bytes, re-parses to detect the "no RAR" case, embeds the array as a JWT custom claim, and stamps the token-response field. Push-mode `dispatchPushApproval` mirrors the same field in the callback payload (§5.2 over the push delivery channel). Embedding is factored into an `embedRARForToken` helper that filters empty/`[]`/`null` so legacy CIBA tokens are not contaminated with empty arrays. - `internal/service/oauth.go` — `OAuthService.Introspect` adds `authorization_details` to the surfaced-claim allowlist per §7. ## Legacy preservation A CIBA request that does NOT supply `authorization_details` produces a token whose response body, JWT, and introspection result all omit the field — clients can branch on `present? → typed grant` without false positives. Pinned by `TestCIBA_RAR_TokenSideAbsentForLegacyFlow` and `TestRFC9396_S5_2_TokenResponseOmitsWhenNotGranted`. ## Test plan - `go build ./...` and `go vet ./...` — clean - `go test ./...` — all packages green: * 2 new integration tests in `ciba_rar_test.go`: end-to-end token-side flow asserting body + JWT + introspection agree on the granted payload; legacy CIBA preservation test pinning the negative contract on all three surfaces. * 4 new compliance tests in `rar_compliance_test.go`: TestRFC9396_S5_2_TokenResponseIncludesAuthorizationDetails, TestRFC9396_S5_2_TokenResponseOmitsWhenNotGranted, TestRFC9396_S6_1_AccessTokenJWTEmbedsAuthorizationDetails, TestRFC9396_S7_IntrospectionExposesAuthorizationDetails. * All 14 pre-existing TestCIBA_RAR_* and TestRFC9396_* assertions still pass. ## Docs - `README.md` — Roadmap "Released" entry updated to reflect full RAR (the prior "bc-authorize side only; token-side ships in a follow-up" note is dropped). - `docs/rar.md` — replaces the "What's NOT in this PR" section with a Token-side wiring section explaining the three surfaces, an example introspection response, and the legacy-CIBA non-contamination guarantee. - `tests/integration/COMPLIANCE.md` — RFC 9396 coverage row flipped from "Partial" to "Covered". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
a7f6847 to
600c7cd
Compare
Summary
Completes the RFC 9396 implementation begun in #164. When a CIBA request is approved and a token is issued, the granted
authorization_detailspayload is surfaced on three coordinated surfaces:authorization_detailsfield on/oauth2/tokensuccess responseauthorization_detailsclaim on the issued JWTauthorization_detailsfield on/oauth2/token/introspectAll three surfaces carry the verbatim bytes the client supplied on bc-authorize. zeroid does not modify granted RAR in this release. Legacy CIBA flows (no
authorization_detailson bc-authorize) get the canonical empty array[]on all three surfaces; consumers branch onlen(authorization_details) > 0to detect "actual typed grant in effect."What ships
domain/token.go—AccessToken.AuthorizationDetails json.RawMessageon the token response shape.internal/service/backchannel.go:issueTokenForApprovedRowpassesrow.AuthorizationDetailsRawstraight through into the JWT custom claim and the token-response field. The bc-authorize-side validator (parseAndValidateAuthorizationDetails) already guarantees the persisted bytes are a valid RFC 9396 array, so no re-parse on issuance.dispatchPushApprovalmirrors the same field in the push-callback payload (§5.2 over the push delivery channel).internal/service/oauth.go—OAuthService.Introspectaddsauthorization_detailsto the surfaced-claim allow-list per §7.domain/backchannel_auth.go— loweredMaxAuthorizationDetailsBytesfrom 64 KiB to 4 KiB in response to Copilot review. 4 KB RAR + base64 (~33%) + standard ZeroID claims (~500 B) keeps the totalAuthorization: Bearer …header at ~6.5 KB — safely under nginx (~8 KB default) and ALB (~16 KB). Deployers needing larger payloads use/oauth2/token/introspect(no size constraint there) or reference an out-of-band record by ID.Related
Test plan
🤖 Generated with Claude Code