feat(mcp): accept RFC 8693 delegated tokens at /mcp - #773
Merged
Conversation
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.
This was referenced Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An agent could not ask Authorizer about its own delegated authority:
check_permissionswas 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.
ValidateDelegatedAccessTokenhas 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
ValidateDelegatedAccessTokenForResourcewith 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
ValidateMCPAccessTokentries stateful first, then falls back — gated on theactclaim 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.validateDelegatedForAudienceis the shared core;expectedAudis the only thing that varies. The issuer check still usesparsers.GetHost—issis always the bare URL regardless of the bound resource.TestExposedMCPToolSetIsPinnedguards a latent hazard: delegated tokens carry nononce, harmless for the four current tools but not for anything reachingDeleteUserSession.Verification
mcp_delegated_test.gointernal/token/→ positives fail, negatives still passmake smokemcp_delegatedsubtest;mcp_stdiounaffected✔ Connected;check_permissionsreturned the agent's intersected answer (allowed:falsefor a doc the user can read but the agent was never granted). Static-token path still✔ Connected— no regressionmake test/make lintOnly the Go integration tests were run against the unfixed build; the other layers exercise the same validator.
Not addressed
Docs: authorizerdev/docs#PENDING · Example: authorizerdev/examples#PENDING