Skip to content

feat: RFC 9396 Rich Authorization Requests for CIBA (bc-authorize side) - #164

Merged
saucam merged 1 commit into
mainfrom
feat/rar-authorization-details
May 26, 2026
Merged

feat: RFC 9396 Rich Authorization Requests for CIBA (bc-authorize side)#164
saucam merged 1 commit into
mainfrom
feat/rar-authorization-details

Conversation

@saucam

@saucam saucam commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds RFC 9396 authorization_details support to the CIBA /oauth2/bc-authorize endpoint. Clients can now pass a typed JSON array describing exactly what is being authorized at finer granularity than scope — e.g. a specific tool call with bound parameters — and the BackchannelNotifier hook receives the parsed typed slice so deployer-supplied approver UX can render typed approval prompts (vs the legacy "approve this scope" shape).

Scope is bc-authorize-side only. Token-side wiring (RFC 9396 §7: embed approved authorization_details in the access-token JWT claims and in introspection responses) is a follow-up PR so resource servers can read approved details for receipt-chain commitment. This PR is everything AuthN needs to begin its BackchannelNotifier implementation.

What ships

  • migrations/027_rar_authorization_details.{up,down}.sql — JSONB column on backchannel_auth_requests with NOT NULL DEFAULT '[]'::jsonb so pre-RAR rows surface as [] and consumer code stays branch-free.
  • domain/backchannel_auth.goAuthorizationDetail / AuthorizationDetails types preserving each element's raw JSON verbatim; ParseAuthorizationDetails enforces RFC 9396 outer shape (array of objects, each with non-empty string type field); 64 KB per-request cap; ErrAuthorizationDetailsMalformed + ErrAuthorizationDetailsOversized sentinels for errors.Is.
  • internal/service/backchannel.go — request-input field, size cap, parse + validate, per-type validator registry guarded by RWMutex, dispatchNotifierWithRAR threading the typed slice into the notifier payload, backward-compatible dispatchNotifier that re-parses the persisted column for callers that don't carry typed context.
  • hooks.goBackchannelNotification.AuthorizationDetails field; AuthorizationDetailValidator type (the public surface deployers register against).
  • server.goServer.RegisterAuthorizationDetailValidator(typ, fn) at the top-level package.
  • internal/handler/oauth.goBcAuthorizeInput.AuthorizationDetails field; pass-through to the service.

Validation depth

Permissive by default. zeroid validates only:

  • Outer shape: JSON array of objects.
  • Per-element shape: each object has a type field that is a non-empty string.

Per-type schema validation is opt-in via Server.RegisterAuthorizationDetailValidator(typ, fn). This matches how Auth0 and Okta handle CIBA RAR — the library accepts the shape, the deployer (AuthN in Highflame's stack) layers strict type-aware validation on top. Strict deployers can register a validator per accepted type; an unregistered type passes outer-shape validation and proceeds to the notifier.

Rejections return the OAuth error code invalid_authorization_details per RFC 9396 §5.4 — distinct from invalid_request so clients can branch on the error code, not the description string.

What does NOT ship (follow-up PR 2)

  • Approved authorization_details propagated into the access-token JWT claims (RFC 9396 §7).
  • Approved authorization_details included in /oauth2/token/introspect responses.
  • Resource-server reflection helpers (zeroid SDK side, for clients reading approved details out of an introspected token).

Out of scope (separate efforts)

  • GIN index on the JSONB column — nothing queries by authorization_details content today. Add when usage emerges.
  • Strict type registry inside zeroid — the permissive + opt-in validator pattern was chosen explicitly in ADR 0002 (CIBA + RAR for AARM STEP_UP).

Related

Test plan

  • go build ./... and go vet ./... clean
  • go test ./... — all packages green
  • 19 new domain sub-tests in ParseAuthorizationDetails (backward-compat: nil/empty/null/[]; outer-shape failures: object/string/number/truncated/trailing-comma; per-element failures: missing/empty/non-string/null type, non-object/string/nested-array element; raw-bytes preservation of unknown fields)
  • 4 new integration test functions in ciba_rar_test.go (notifier receives parsed details; backward-compat omission; malformed → invalid_authorization_details; per-type validator happy path + reject + unregistered-type pass-through)
  • Pre-existing TestCIBA_PollingLifecycle, ping mode, push mode, and compliance suites still pass

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for RFC 9396 OAuth 2.0 Rich Authorization Requests (RAR) in the CIBA flow, allowing clients to send detailed authorization requests via the authorization_details parameter. The changes include database migrations to add a JSONB column, parsing and validation logic, integration with the backchannel notifier, and support for registering custom per-type validators. The review feedback highlights a discrepancy in ParseAuthorizationDetails where the implementation does not match the documented behavior of treating whitespace-only and literal JSON null as empty payloads, and suggests using the bytes package to trim and check the input correctly.

Comment thread domain/backchannel_auth.go
Comment thread domain/backchannel_auth.go
@saucam
saucam force-pushed the feat/rar-authorization-details branch from 1058ee1 to f9274d0 Compare May 26, 2026 01:35
@saucam

saucam commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Auto-fixed by pr-shepherd (iteration 1):

  • highflame-lint-check (gofmt): re-aligned struct tags on domain.BackchannelAuthRequest after the AuthorizationDetailsRaw field's preceding doc comment broke gofmt's tag-column alignment for the prior fields. Pure whitespace; no behavior change.

Local go build ./..., go vet ./..., go test ./domain/... ./internal/... ./ all green. Re-running CI.

@saucam
saucam force-pushed the feat/rar-authorization-details branch from f9274d0 to f7ba547 Compare May 26, 2026 01:39
@saucam

saucam commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Auto-fixed by pr-shepherd (iteration 2) — addressing Gemini review (CI was already green at end of iteration 1):

  • domain/backchannel_auth.go — added bytes import; ParseAuthorizationDetails now trims via bytes.TrimSpace and explicitly returns (nil, nil) for empty / whitespace-only / literal null bytes before json.Unmarshal. The function's doc comment already promised this; the implementation now matches.
  • domain/backchannel_auth_test.go — added 5 sub-tests under TestParseAuthorizationDetails_BackwardCompatible (single space, multiple spaces, newline, mixed whitespace, null with surrounding whitespace) to pin the contract.

Whitespace-only input is unreachable from the HTTP path today (Huma's JSON decoder never hands us bare whitespace bytes), but the doc/code mismatch was real — kept the fix minimal and defensive. No behavior change for any previously-tested input (nil, empty, null 4-byte, [], single-element, multi-element, all outer/element-shape rejection cases) — verified locally via go test ./domain/....

Local go build ./..., go vet ./..., go test ./domain/... ./internal/... ./ all green. Iteration 1 CI was 9/9 green; re-running to confirm the Gemini fix keeps it that way.

@saucam

saucam commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

pr-shepherd — all checks green and Gemini comments addressed.

CI status: 9/9 SUCCESS (build, lint, docker, integration, Python SDK, TypeScript SDK, Trivy, Socket Security x2).

Gemini review feedback (1 round, 2 medium-priority comments):

  • domain/backchannel_auth.gobytes import + TrimSpace / explicit null check in ParseAuthorizationDetailsapplied (matches existing doc comment; whitespace-only is unreachable from the HTTP path today, but the doc/code mismatch was real).
  • Whitespace-only input handling → applied with 5 new sub-tests pinning the contract.

Design choices preserved (no scope creep):

  • No GIN index (out of scope — nothing queries by content yet).
  • Permissive default validation + opt-in per-type validator (ADR 0002).
  • Token-side wiring (JWT embed + introspection) explicitly deferred to PR 2.

Ready for human review. reviewDecision: REVIEW_REQUIRED is the expected human gate.

@saucam
saucam force-pushed the feat/rar-authorization-details branch from f7ba547 to 7b68d5e Compare May 26, 2026 02:15
@rsharath
rsharath requested a review from Copilot May 26, 2026 02:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds RFC 9396 Rich Authorization Requests (RAR) support to the CIBA /oauth2/bc-authorize flow, allowing clients to submit structured authorization_details that are parsed/validated (outer-shape + optional per-type validators) and forwarded to the BackchannelNotifier for typed approval UX.

Changes:

  • Introduces domain types + parser for authorization_details (outer shape enforcement + size cap + sentinel errors).
  • Threads RAR from handler → service → persistence → notifier, including opt-in per-type validator registration.
  • Extends form-body compatibility middleware to preserve JSON-shaped OAuth parameters (so form-encoded RAR works), and adds integration + domain tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/integration/ciba_rar_test.go New integration coverage for RAR happy path, malformed handling, per-type validators, panic mapping, and form-encoded requests.
server.go Adds public validator registration API and updates OAuth form-compat middleware to support JSON-shaped form fields (RAR).
migrations/027_rar_authorization_details.up.sql Adds authorization_details JSONB column (NOT NULL, default []) to persist RAR on backchannel requests.
migrations/027_rar_authorization_details.down.sql Drops the authorization_details column.
internal/service/backchannel.go Parses/validates RAR, runs optional per-type validators, and includes parsed details in notifier payload.
internal/handler/oauth.go Adds authorization_details request field and forwards raw JSON bytes into the service layer.
hooks.go Extends notifier payload with parsed AuthorizationDetails and exposes validator function type.
domain/backchannel_auth.go Adds RAR domain types + parser and adds AuthorizationDetailsRaw persistence field on BackchannelAuthRequest.
domain/backchannel_auth_test.go New unit tests for ParseAuthorizationDetails behavior and error classification.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread domain/backchannel_auth.go Outdated
Comment thread hooks.go Outdated
Comment thread server.go Outdated
@rsharath

rsharath commented May 26, 2026

Copy link
Copy Markdown
Contributor

@saucam can you please update README and add any associated docs? Also, add compliance tests: same pattern as the other RFCs in the standards table (#154 / discovery_compliance / prm_compliance), add tests/integration/rar_compliance_test.go with the TestRFC9396_S<section>_<descriptor> naming.

@saucam
saucam force-pushed the feat/rar-authorization-details branch from 7b68d5e to 2263145 Compare May 26, 2026 02:40
@saucam

saucam commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Done — pushed in the latest amend.

Compliance suite: tests/integration/rar_compliance_test.go — 10 assertions following the same TestRFC9396_S<section>_<descriptor> pattern as ciba_compliance_test.go / discovery_compliance_test.go. One MUST per test, first body line cites the spec clause, RFC-order layout (§2 → §2.1 → §3 → §5). Scope is bc-authorize-side only — token-side clauses (§5 token response, §6.1 JWT claim, §7 introspection) will extend this file in lockstep with the token-embed PR.

Coverage matrix: Added the RFC 9396 row to tests/integration/COMPLIANCE.md marked Partial with the same note.

README:

  • Added a Core Capabilities bullet (right after the CIBA bullet) summarizing the RAR shape with a concrete {"type": "tool_call", "tool": "transfer_funds", ...} example.
  • Added a Rich Authorization Requests · RFC 9396 row to the Standards table.
  • Added a Roadmap "Released" bullet flagging "bc-authorize side; token-side ships in follow-up."

New reference doc: docs/rar.md — same style as docs/dpop-and-dcr.md. Covers the wire shape, multi-element semantics, the permissive-default + opt-in per-type validator model with a Go snippet, notifier integration, both content types with curl-style examples, persistence shape, the full error-code mapping table, and what does/doesn't ship in this PR. The README's RAR bullet links to it.

Local verification: gofmt/go vet/go build/go test ./... all green. All 10 new compliance tests pass alongside the existing 8 TestCIBA_RAR_* integration tests.

Adds RFC 9396 `authorization_details` support to the CIBA bc-authorize
endpoint. Clients can now pass a typed JSON array describing exactly
what is being authorized at finer granularity than `scope` — e.g. a
specific tool call with bound parameters — and the BackchannelNotifier
hook receives the parsed typed slice so deployer-supplied approver UX
can render typed approval prompts (vs the legacy "approve this scope"
shape).

Scope of this PR is bc-authorize-side only. Token-side wiring (RFC 9396
§7: include approved `authorization_details` in the access-token JWT
claims and in introspection responses) is a follow-up so resource
servers can read approved details for receipt-chain commitment. This
PR is everything AuthN needs to begin its BackchannelNotifier
implementation.

## What ships

- `migrations/027_rar_authorization_details.{up,down}.sql` — adds a
  JSONB `authorization_details` column on `backchannel_auth_requests`
  with `NOT NULL DEFAULT '[]'::jsonb` so pre-RAR rows surface as an
  empty array and consumer code stays branch-free.
- `domain/backchannel_auth.go` — `AuthorizationDetail` /
  `AuthorizationDetails` types preserving each element's raw JSON
  verbatim; `ParseAuthorizationDetails` enforces RFC 9396 outer shape
  (array of objects, each with a non-empty string `type` field);
  `MaxAuthorizationDetailsBytes = 64 KB` per-request cap;
  `ErrAuthorizationDetailsMalformed` + `ErrAuthorizationDetailsOversized`
  sentinels for errors.Is.
- `internal/service/backchannel.go` — request-input field, size cap,
  parse + validate path, per-type validator registry guarded by
  RWMutex, `dispatchNotifierWithRAR` that threads the typed slice
  through to the notifier payload. Validator invocation wrapped in
  `runRARValidator` so a buggy deployer-registered validator (panic
  or nil-deref) maps to invalid_authorization_details instead of
  escaping as HTTP 500 via chi's Recoverer.
- `hooks.go` — `BackchannelNotification.AuthorizationDetails` field;
  `AuthorizationDetailValidator` type — the public surface deployers
  register against.
- `server.go` — `Server.RegisterAuthorizationDetailValidator(typ, fn)`
  exposed at the top-level package; the existing
  `SetBackchannelNotifier` wrapping now threads the typed slice
  through. `oauthFormCompatMiddleware` special-cases JSON-shaped
  form fields (currently `authorization_details`) so form-encoded
  clients per RFC 9396 §2.1 see their JSON arrays bind correctly,
  not get string-flattened by the default form→JSON bridge.
- `internal/handler/oauth.go` — `BcAuthorizeInput.AuthorizationDetails`
  field; pass-through to the service.

## Validation depth

Permissive by default. zeroid validates only:

  - Outer shape: JSON array of objects.
  - Per-element shape: each object has a `type` field that is a
    non-empty string.

Per-type schema validation is opt-in via the
`Server.RegisterAuthorizationDetailValidator(typ, fn)` hook. This
matches how Auth0 and Okta handle CIBA RAR — the library accepts the
shape, the deployer (here, AuthN in Highflame's stack) layers strict
type-aware validation on top. Strict deployers can register a
validator for every accepted type; an unregistered type passes
outer-shape validation and proceeds to the notifier.

Rejections at any layer return the OAuth error code
`invalid_authorization_details` per RFC 9396 §5.4 — distinct from
`invalid_request` so clients can branch on the error code rather than
the description string. This includes deployer-validator panics,
which are caught and surfaced as invalid_authorization_details with
"validator panicked: ..." in the description.

## Content-type support

Both `application/json` and `application/x-www-form-urlencoded`
request bodies are supported per RFC 9396 §2.1. The form middleware
detects JSON-shaped fields (authorization_details today; extensible
via `jsonShapedFormFields`) and passes them through as raw JSON so
the downstream binder sees the original array shape instead of the
default string-flatten.

## What does NOT ship (follow-up PR 2)

- Approved `authorization_details` propagated into the access-token
  JWT claims (RFC 9396 §7 / token-side).
- Approved `authorization_details` included in `/oauth2/token/introspect`
  responses.
- Resource-server reflection helpers (zeroid SDK side, for clients
  reading the approved details out of an introspected token).

## Out of scope (separate efforts)

- A GIN index on the JSONB column — nothing queries by
  authorization_details content today. Add when usage emerges.
- Strict type registry inside zeroid — the permissive + opt-in
  validator pattern was chosen explicitly in
  highflame-architecture/adrs/0002 (CIBA + RAR for AARM STEP_UP).

## Verification

- `go build ./...` and `go vet ./...` — clean
- `go test ./...` — all packages green:
  * 24 domain sub-tests in `ParseAuthorizationDetails` covering
    backward-compatible inputs (nil, empty, null, [], whitespace),
    outer-shape failures, per-element failures, and round-trip
    preservation of unknown fields in `Raw`.
  * 8 integration test functions in `ciba_rar_test.go`: notifier
    receives parsed details, backward-compatible omission, explicit
    empty array, malformed cases mapped to invalid_authorization_details,
    per-type validator end-to-end, validator panic surfacing as the
    OAuth error code, form-encoded RAR happy path, form-encoded
    malformed surfacing the OAuth error code.
  * Pre-existing `TestCIBA_PollingLifecycle`, ping mode, push mode,
    and compliance suites all still pass.

## Related

- ADR 0002 (AARM STEP_UP uses CIBA + RAR): https://github.com/highflame-ai/highflame-architecture/blob/main/adrs/0002-aarm-stepup-defer-protocol.md
- Wave B v1 (Shield-side annotation validation): highflame-ai/highflame-shield#193 (merged)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@saucam
saucam force-pushed the feat/rar-authorization-details branch from 2263145 to 806f6d5 Compare May 26, 2026 02:46
@saucam
saucam merged commit f10454f into main May 26, 2026
9 checks passed
saucam added a commit that referenced this pull request May 26, 2026
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>
saucam added a commit that referenced this pull request May 26, 2026
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>
saucam added a commit that referenced this pull request May 26, 2026
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>
saucam added a commit that referenced this pull request May 26, 2026
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>
saucam added a commit that referenced this pull request May 27, 2026
…on) (#168)

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants