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
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 checksaud == <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
resourceis 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 inOidcController#valid_resource_indicator?(absolute URI, no fragment). - Introspection returns
aud = access_token.resourcewhen bound, falling back to the client_id when no resource indicator was used. - Binding happens at authorization time and is carried through; token-time
resourcenarrowing 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.