Repository navigation
Access Token Privilege Restriction: does "MUST NOT pass through" cover an API of the same authorization server/project (Supabase Edge MCP pattern)? #3413
Description
What's broken?
The spec is ambiguous or self-contradictory
Where in the spec or docs?
What should happen?
The spec should state clearly whether "The MCP server MUST NOT pass through the token it received from the MCP client" applies when the "upstream" API is part of the same authorization server / same protected-resource domain as the MCP server. The motivating case is a thin, user-scoped MCP adapter over its own project's Data API.
If that case is permitted, the spec should say what the server must verify, for example:
- the audience or resource indicator naming the MCP server (RFC 8707);
- the client binding;
- that the Data API is listed as part of the same protected resource in RFC 9728 metadata.
If it is not permitted, the spec should name the recommended pattern: a separately consented downstream grant, or RFC 8693 token exchange even when the AS offers no downstream-audience exchange.
What actually happens?
The normative text has no same-domain carve-out. A widely used vendor pattern does exactly this, so implementers get contradictory guidance.
Supabase's first-party guides document the following pattern:
- Deploy MCP servers and MCP Authentication describe an MCP server running as an Edge Function inside the project.
- Supabase Auth acts as the OAuth 2.1 AS, with RFC 9728 discovery, PKCE and DCR.
- The function verifies the user's token via JWKS, then sends that same token to the project's REST Data API so Postgres Row Level Security applies.
We maintain Supabase User MCP, an open-source, user-scoped data-plane MCP server: RLS decides, and no privileged key is in the request path. Our design records reject the vendor pattern on the strength of this MUST NOT:
The compliant alternative needs a second consent and encrypted per-user grant storage for every deployment. That is a real adoption cost, and it diverges from the vendor's guidance. We would rather follow the spec's intent than guess.
A related inbound-side observation from a local test: Supabase CLI 2.119.0 / Auth v2.197.0 with the OAuth 2.1 server enabled ignores the RFC 8707 resource parameter. Issued tokens carry aud=authenticated with or without resource=<MCP URL>. So the inbound rule ("MUST reject tokens that do not include them in the audience claim or otherwise verify that they are the intended recipient") also has no obvious native way to be satisfied in this setup. Clarifying what "otherwise verify that they are the intended recipient" accepts would help here too.
Anything else?
Specific questions:
- Is the MUST NOT aimed at tokens for third-party or otherwise separately protected APIs, or does it also cover an API of the same AS and project as the MCP server?
- If same-domain use is allowed, which verification is required (RFC 8707 audience, client binding, RFC 9728 metadata listing)?
- If not allowed, is a separately consented downstream grant the expected pattern when the AS has no downstream token exchange?
- Would a clarifying note or example for the "thin user-scoped adapter over its own AS's API" case be welcome? We can draft one.
Closest prior discussion: #1247 (not resolved by a maintainer).
AI disclosure: this issue was drafted by an AI assistant (Anthropic Claude, running in the Hermes agent framework) from our project's design records and a local test. The architecture review was assisted by OpenAI ChatGPT. I (repository owner) directed the question and authorized posting it, and I'll follow up in this thread.
Activity
jryski commentedon Oct 4, 2026
Follow-up from the reporter's side: while this question is open, we wrote down the position we intend to take, with the full reasoning, as a draft in our repository. That way it can be critiqued concretely:
In short, the authorization server issues a token whose audience is the MCP resource, and its role grants database privileges for only the functions behind the MCP tools. RLS, bounds and live session and client checks are enforced in Postgres. So the token can do nothing more by bypassing the MCP server than by going through it.
We do not claim this satisfies the literal "separate token" wording. We record it as a disclosed deviation and keep a separate-credential mode for strict conformance. A maintainer ruling either way would settle which mode we treat as primary.
AI disclosure: drafted by an AI assistant (Anthropic Claude, in the Hermes agent framework); posted with the repository owner's authorization.
cc-bb-aa commentedon Oct 4, 2026
Running an MCP gateway in production, the confused-deputy angle is where this bites in practice: a client that forwards its token to a server which then calls an upstream API has effectively minted a broader credential than the user authorized, and nobody in the chain can prove what scope the original grant had. Audience binding per hop (each server gets a token scoped to itself, with an exchange at the boundary if it needs upstream access) removes the ambiguity without changing the spec's intent. It also gives you a per-hop audit point, which you will want the first time a server does something unexpected with downstream data.
sattyamjjain commentedon Oct 5, 2026
Adding the scanner side, since the ruling decides what static tools should flag.
I maintain agent-audit-kit. Its token passthrough rule (AAK-OAUTH-003) is critical, and its remediation is "never forward a user token across trust boundaries". A static check can't tell a same-AS Data API from a third-party one, so without a carve-out, the Supabase Edge pattern is a true positive. If same-authority use is allowed, tools need conditions they can actually check, and your three are the right ones:
One note: your observation that Supabase Auth ignores
resourceand issues aud=authenticated means (1) fails in that setup even with a carve-out. I'd keep that as a separate question so it doesn't block the main ruling.jryski commentedon Oct 5, 2026
Thank you both, this is really helpful. You're describing the same problem from two useful angles: what a correct design looks like, and what a tool can actually verify. Our goal is to make whatever the maintainers decide easy to implement and easy to check, so here is everything we've learned so far, in case it helps.
@cc-bb-aa: agreed. Per-hop audience binding, with an exchange at the boundary, is the clean model, and it's the one we'd reach for first. The reason we wrote this up is that not every authorization server can do that exchange yet. Supabase Auth, for example, has no RFC 8693 token exchange for a downstream audience today. Implementers on platforms like that end up choosing between holding a second, separately consented credential on the server, or forwarding. We'd like the spec to give them a clear and safe answer either way.
@sattyamjjain: thank you for bringing the scanner perspective. Whatever the ruling is, framing it as conditions a tool can check seems exactly right. Here's where each of your three conditions stands in our draft, on Supabase:
The token's
audnames the MCP server's canonical URI. You're right that Supabase ignoresresourceby default. We found it can still be met: Supabase's Custom Access Token Hook can setaud. In our draft the hook setsaudto the single canonical MCP URI for approved MCP clients only, on both issuance and refresh, and those tokens never carry the generalauthenticatedaudience. So this condition can be satisfied today with some setup. We agree it's worth tracking separately as a request for nativeresourcesupport.The token is bound to the client the user consented to. On every call, the backend re-checks
client_idagainst a server-controlled list of approved clients, and checks that the token'ssession_idis still a live session for the same user and client. In our local tests, revoking access was enforced on the very next call.The downstream API is listed as part of the same protected resource in RFC 9728 metadata. This is the one we couldn't make checkable. RFC 9728 metadata describes a single
resource, and we didn't find a standard way to say "these upstream endpoints belong to the same protected resource, under the same issuer." Our draft requires this to be documented, but documentation isn't something a scanner can read. Purely as an illustration of the shape (not a proposal of a name), something like:{ "resource": "https://<project>.supabase.co/functions/v1/mcp", "authorization_servers": ["https://<project>.supabase.co/auth/v1"], "same_resource_endpoints": ["https://<project>.supabase.co/rest/v1/rpc/list_own_v1"] }would let a tool confirm that the downstream shares the issuer and was declared on purpose. If the maintainers think this direction is worth exploring, we'd be glad to help draft it properly. We're equally happy to follow a different approach if one fits better.
We'd also suggest one more condition for anyone considering a same-authority allowance. The forwarded token shouldn't be able to do more at the downstream than the MCP tools expose. In our draft, that means:
Without something like this, a correct audience still lets a client go around the server's controls. That's the risk the current rule protects against.
To be clear about where our draft stands:
The draft and its test evidence are here if useful: jryski/Supabase_user_MCP#102
AI disclosure: drafted by an AI assistant (Anthropic Claude, in the Hermes agent framework) and posted with the repository owner's authorization.
vladzdev commentedon Oct 8, 2026
I’d make the distinction around the resource the token is authorized for rather than around whether the upstream happens to share the same authorization server or domain. The risky case is forwarding a token outside its intended audience, while a same-project data API can still be a legitimate resource boundary if that relationship is explicit and verifiable. Otherwise implementations are forced to infer intent from deployment topology, which seems like the wrong place to put the security rule.