feat: RFC 9396 Rich Authorization Requests for CIBA (bc-authorize side) - #164
Conversation
There was a problem hiding this comment.
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.
1058ee1 to
f9274d0
Compare
|
Auto-fixed by pr-shepherd (iteration 1):
Local |
f9274d0 to
f7ba547
Compare
|
Auto-fixed by pr-shepherd (iteration 2) — addressing Gemini review (CI was already green at end of iteration 1):
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 ( Local |
|
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):
Design choices preserved (no scope creep):
Ready for human review. |
f7ba547 to
7b68d5e
Compare
There was a problem hiding this comment.
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.
7b68d5e to
2263145
Compare
|
Done — pushed in the latest amend. Compliance suite: Coverage matrix: Added the RFC 9396 row to README:
New reference doc: Local verification: |
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>
2263145 to
806f6d5
Compare
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>
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>
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>
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>
…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>
Summary
Adds RFC 9396
authorization_detailssupport to the CIBA/oauth2/bc-authorizeendpoint. Clients can now pass a typed JSON array describing exactly what is being authorized at finer granularity thanscope— e.g. a specific tool call with bound parameters — and theBackchannelNotifierhook 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_detailsin 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 itsBackchannelNotifierimplementation.What ships
migrations/027_rar_authorization_details.{up,down}.sql— JSONB column onbackchannel_auth_requestswithNOT NULL DEFAULT '[]'::jsonbso pre-RAR rows surface as[]and consumer code stays branch-free.domain/backchannel_auth.go—AuthorizationDetail/AuthorizationDetailstypes preserving each element's raw JSON verbatim;ParseAuthorizationDetailsenforces RFC 9396 outer shape (array of objects, each with non-empty stringtypefield); 64 KB per-request cap;ErrAuthorizationDetailsMalformed+ErrAuthorizationDetailsOversizedsentinels forerrors.Is.internal/service/backchannel.go— request-input field, size cap, parse + validate, per-type validator registry guarded byRWMutex,dispatchNotifierWithRARthreading the typed slice into the notifier payload, backward-compatibledispatchNotifierthat re-parses the persisted column for callers that don't carry typed context.hooks.go—BackchannelNotification.AuthorizationDetailsfield;AuthorizationDetailValidatortype (the public surface deployers register against).server.go—Server.RegisterAuthorizationDetailValidator(typ, fn)at the top-level package.internal/handler/oauth.go—BcAuthorizeInput.AuthorizationDetailsfield; pass-through to the service.Validation depth
Permissive by default. zeroid validates only:
typefield 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_detailsper RFC 9396 §5.4 — distinct frominvalid_requestso clients can branch on the error code, not the description string.What does NOT ship (follow-up PR 2)
authorization_detailspropagated into the access-token JWT claims (RFC 9396 §7).authorization_detailsincluded in/oauth2/token/introspectresponses.Out of scope (separate efforts)
authorization_detailscontent today. Add when usage emerges.Related
Test plan
go build ./...andgo vet ./...cleango test ./...— all packages greenParseAuthorizationDetails(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)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)TestCIBA_PollingLifecycle, ping mode, push mode, and compliance suites still pass🤖 Generated with Claude Code