Skip to content

feat(mcp): accept RFC 8693 delegated tokens at /mcp - #773

Merged
lakhansamani merged 1 commit into
mainfrom
feat/mcp-delegated-tokens
Aug 15, 2026
Merged

feat(mcp): accept RFC 8693 delegated tokens at /mcp#773
lakhansamani merged 1 commit into
mainfrom
feat/mcp-delegated-tokens

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

An agent could not ask Authorizer about its own delegated authority: check_permissions was unreachable with the very token that proves the delegation, so agent-delegation over MCP stayed a stdio-only story.

The invariant

f(aud) = surface, and it stays a bijection — every token valid at exactly one surface.

The obvious implementation breaks it. ValidateDelegatedAccessToken has exactly one caller, GetUserIDFromSessionOrAccessToken, which backs /graphql, /v1/* and gRPC. Relaxing its audience check in place would have turned every MCP-bound delegated token into a full first-party credential.

So the fallback goes through a new ValidateDelegatedAccessTokenForResource with an exact match — never "hostname or resource", which would break the bijection from the other side by letting a first-party token open /mcp.

Changes

  • ValidateMCPAccessToken tries stateful first, then falls back — gated on the act claim so an ordinary wrong-audience token is rejected once instead of paying a second validation (parse + session lookup + subject-liveness DB read) on an internet-facing endpoint.
  • validateDelegatedForAudience is the shared core; expectedAud is the only thing that varies. The issuer check still uses parsers.GetHostiss is always the bare URL regardless of the bound resource.
  • TestExposedMCPToolSetIsPinned guards a latent hazard: delegated tokens carry no nonce, harmless for the four current tools but not for anything reaching DeleteUserSession.

Verification

Layer Result
mcp_delegated_test.go 14 subtests — reachability, bijection both ways, FGA intersection
Fail-without-fix Stashed internal/token/ → positives fail, negatives still pass
Playwright (compose stack) 3 tests incl. browser OAuth + intersection; 12 existing MCP/CIMD/DCR tests green
make smoke New mcp_delegated subtest; mcp_stdio unaffected
Real Claude Code 2.1.233 ✔ Connected; check_permissions returned the agent's intersected answer (allowed:false for a doc the user can read but the agent was never granted). Static-token path still ✔ Connected — no regression
make test / make lint Zero failures / 0 issues

Only the Go integration tests were run against the unfixed build; the other layers exercise the same validator.

Not addressed

  • The 5-minute/no-refresh 401. A delegated token has no refresh token, so the discovery challenge tells a client to do something it cannot. Documented rather than changed — fixing it means a distinct error code or a longer TTL, both separate decisions.
  • Double validation (middleware + interceptor) — pre-existing, now slightly heavier for delegated tokens.
  • The stdio subcommand and its smoke subtest — untouched until 2.5.0.

Docs: authorizerdev/docs#PENDING · Example: authorizerdev/examples#PENDING

An agent could not ask Authorizer about its own delegated authority:
check_permissions was unreachable with the very token that proves the
delegation, so agent-delegation over MCP stayed a stdio-only story.

ValidateMCPAccessToken falls back to a delegated check when the
stateful one fails, gated on the `act` claim so an ordinary
wrong-audience token is rejected once rather than paying a second
validation on an internet-facing endpoint.

The fallback goes through a new entry point,
ValidateDelegatedAccessTokenForResource, rather than widening
ValidateDelegatedAccessToken. That function has one caller —
GetUserIDFromSessionOrAccessToken — which backs /graphql, /v1/* and
gRPC, so relaxing its audience check in place would have made every
MCP-bound delegated token a full first-party credential.

The match stays exact in both directions: a token bound to the bare
server URL is still refused at /mcp, and an MCP-bound one is still
refused everywhere else. One token, one surface.
@lakhansamani
lakhansamani merged commit ad96fdf into main Aug 15, 2026
4 checks passed
@lakhansamani
lakhansamani deleted the feat/mcp-delegated-tokens branch August 15, 2026 08:48
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.

1 participant