Files
clinch/docs/decisions/0004-resource-indicators.md
Dan MilneandClaude Opus 4.8 2defa26a87 Add RFC 8707 Resource Indicators (audience binding)
Clients can name a target resource server via the `resource` parameter at the
authorization and device-authorization endpoints. Clinch binds it to the issued
token as its audience and reports it at introspection as `aud`, so a token minted
for one API cannot be replayed against another that also trusts clinch.

- `resource` is validated syntax-only (absolute URI, no fragment) — pass-through,
  no resource registry; the resource server enforces the audience on introspection
- threaded from authorize / device_authorization onto the auth/device code, then
  the access and refresh tokens, and carried across refresh rotation
- invalid values are rejected with error=invalid_target
- introspection `aud` is the bound resource, falling back to the client_id
- ADR 0004 records the pass-through-vs-registry decision

Completes the clinch-side OAuth surface for MCP connectors (with DCR +
introspection); Protected Resource Metadata (RFC 9728) lives on the resource server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
2026-07-19 12:41:10 +10:00

2.6 KiB

0004 — Resource Indicators (RFC 8707), pass-through binding

Status: Accepted · Date: 2026-07-19

Decision

Clinch accepts the RFC 8707 resource parameter at /oauth/authorize and /oauth/device_authorization, binds it to the issued token as its audience, and reports it at introspection as aud. Validation is syntax-only (pass-through): the value must be an absolute URI without a fragment; clinch does not maintain a registry of resource servers. The resource server enforces the audience when it introspects the token.

Context

Without an audience, an access token minted for one API could be replayed against another API that also trusts clinch (the confused-deputy problem). RFC 8707 lets the client name the target (resource=https://c2a2.example.com); the token is then bound to that audience and is useless elsewhere.

Two ways to handle the value:

  • Pass-through (chosen): validate the URI syntax, store it, report it as aud. Enforcement is at the resource server, which already validates tokens via introspection (0001) and simply checks aud == <its own identifier>. A token bound to an arbitrary audience is worthless anywhere that isn't that audience, so no clinch-side registry is needed.
  • Registry-gated (not chosen): reject unknown resources with invalid_target. Catches typos early but requires clinch to model and maintain resource-server identities, which it does not have today.

Implementation notes

  • resource is threaded from the authorize / device_authorization request onto the authorization/device code, then onto the access and refresh tokens, and is carried across refresh rotation so re-issued tokens keep the audience.
  • Invalid resources are rejected with error=invalid_target (redirect for authorize, JSON 400 for device_authorization). Validation lives in OidcController#valid_resource_indicator? (absolute URI, no fragment).
  • Introspection returns aud = access_token.resource when bound, falling back to the client_id when no resource indicator was used.
  • Binding happens at authorization time and is carried through; token-time resource narrowing is not implemented (not needed for the MCP / device flows).

Consequences

  • Tokens can be scoped to a single resource server, closing the cross-service replay path — enforced where it belongs, at the resource server.
  • Completes the clinch-side OAuth surface MCP connectors rely on (with DCR 0003 and introspection). The remaining MCP piece, Protected Resource Metadata (RFC 9728), lives on the resource server.