The MCP authorization spec has a one-line rule that is easy to read past and easy to violate. From the 2025-11-25 authorization spec: “MCP servers MUST only accept tokens specifically intended for themselves and MUST reject tokens that do not include them in the audience claim or otherwise verify that they are the intended recipient of the token.” A few sentences later it closes the loophole on the other side: “The MCP server MUST NOT pass through the token it received from the MCP client.”
Two rules, one property: a token minted for server A is worthless at server B, and a server never lends out a credential it was handed. Miss either one and you have built a confused deputy, an intermediary tricked into wielding its own authority on behalf of the wrong caller.
- You forward a caller's bearer token to an upstream API
- You run more than one MCP server behind one authorization server
- You hand-roll OAuth and check the issuer but not the audience
- You want a leaked token useless on every other server
What goes wrong without audience binding
The confused deputy is old, but MCP gives it a fresh surface. A bearer token is a signed claim: your identity provider vouches for who the holder is. Validating it confirms that signature, but not that the token was minted for this particular server. Skip that second check and two things break, both named in the spec’s security best practices.
A token for one server works at every server. Say you run two MCP servers
behind the same identity provider, a public /mcp/orders and an internal
/mcp/billing. An agent legitimately holds a token for /mcp/orders. If
/mcp/billing checks only the issuer, that orders token sails straight through,
because both servers trust the same signer. The audience claim, the one field
naming which server the token is for, was never read, so the boundary between
public and internal exists on paper only. The spec is blunt: a server that skips
this “may accept tokens originally issued for other services… allowing
attackers to reuse legitimate tokens across different services than intended.”
A forwarded token impersonates the server. Now the /mcp/orders server
passes the caller’s token, unchanged, to the upstream Orders API. The upstream
sees a valid token and answers, with no way to tell it came from an end user
rather than from the server itself. As the spec puts it, “the downstream API may
incorrectly trust the token as if it came from the MCP server.” One stolen
client token is now a key to the upstream too.
Two missing checks, one root cause: nobody asked who the token was for.
Bind the token to one resource with RFC 8707
The fix is an audience, set by the client and enforced by the server. MCP adopts RFC 8707 resource indicators for exactly this. The client names the resource it intends to use the token with, on both the authorization request and the token request, so the authorization server can stamp that audience into the token it issues.
The spec is unambiguous about what that buys you: the resource parameter
“MUST identify the MCP server that the client intends to use the token with”
and “MUST use the canonical URI of the MCP server.” On the receiving end,
the server “MUST validate that tokens presented to them were specifically
issued for their use.” A token stamped for /mcp/orders presented at
/mcp/billing fails audience validation and gets rejected. One token, one
server, no replay.
When a request arrives with no token, or a token for the wrong audience, the
server answers with a 401 that points the client at its protected-resource
metadata, per RFC 9728:
The client fetches that document to learn which authorization server to talk to and which resource to name:
That resource value is the same string the client must echo back in the
resource parameter above. The metadata, the audience, and the token are all
nailed to one URI.
Let the MCP gateway be the OAuth authorization and resource server

Audience binding only holds if the server actually mints and validates tokens to
spec, and that is a stack of overlapping RFCs most teams should not implement
once, let alone once per server. The
Zuplo MCP Gateway, in public beta, does
it at the boundary. For its /mcp/{routePath} routes, per the
auth overview, “the gateway
is both the OAuth 2.1 Resource Server (RS) and the OAuth 2.1 Authorization
Server (AS).”
It implements the stack of standards audience binding depends on, so you don’t:
| Standard | What the gateway handles |
|---|---|
| Dynamic Client Registration (RFC 7591) | Clients register themselves, no manual app setup per client |
| PKCE with S256 | Required when the client is technically capable |
| Protected Resource Metadata (RFC 9728) | The .well-known document that points a client at the right authorization server |
| Authorization Server Metadata (RFC 8414) | Advertises the AS endpoints and capabilities |
| Resource Indicators (RFC 8707) | Binds each token to one resource; “MCP clients MUST include the resource parameter on every authorization and token request” |
Each virtual server, one /mcp/{routePath} route, gets its own
protected-resource document at
/.well-known/oauth-protected-resource/{routePath}, so its tokens are bound to
that one resource.
MCP Gateway Auth Overview
How the gateway acts as OAuth 2.1 AS and RS, binds tokens with RFC 8707 resource indicators, and brokers upstream credentials without passthrough.
The passthrough rule is handled by separation, not discipline. The gateway mints its own downstream token for the agent and brokers a separate upstream credential for the API behind it. Per the docs, “inbound auth headers don’t leak to the upstream, the gateway always uses an independent upstream credential.” The token the agent holds is never the token the upstream sees, so a poisoned or stolen client token has nowhere to be replayed.
You attach the OAuth behavior as an inbound policy on the MCP route, and the
gateway produces the 401, the protected-resource metadata, and the audience
check shown earlier for you. You write none of those flows by hand.
In the portal you add the policy to the MCP route and pick the preset for your existing IdP rather than standing up a second authorization server:
| Identity provider | Policy preset (links to its setup guide) |
|---|---|
| Auth0 | mcp-auth0-oauth-inbound |
| Okta | mcp-okta-oauth-inbound |
| Microsoft Entra ID | mcp-entra-oauth-inbound |
mcp-google-oauth-inbound | |
| Any OIDC issuer | mcp-oauth-inbound |
Cognito, Clerk, Keycloak, Logto, OneLogin, PingOne, and WorkOS have presets too.
Auth0 is the example below, but the shape is the same for every preset. You
point the policy at your tenant with three environment-backed fields, and the
portal saves it as a mcp-auth0-oauth-inbound policy in your git-backed
config/policies.json:

The issuer, JWKS URL, and OAuth endpoints are all derived from auth0Domain, so
you never wire them in by hand. Nothing here sets a per-route audience either:
the binding that rejects a /mcp/orders token at /mcp/billing is automatic,
from each route’s protected-resource metadata and the RFC 8707 resource value.
The Okta, Entra, and Google presets swap auth0Domain for their own tenant
field and behave identically. If your IdP isn’t on the list, the generic
mcp-oauth-inbound policy takes a standard OIDC issuer URL and gives you the
same audience binding and credential brokering.
Common mistake:
Validating only the issuer is the trap. Two MCP servers behind the same IdP
share an issuer, so an issuer-only check lets a token minted for one sail
through the other. The audience claim (the JWT aud) is what tells them
apart. Check it on every request.
Implement OAuth once, at the boundary
The reason to put this at a gateway is the same reason Anthropic gives for not hand-rolling isolation primitives, which we walked through in Anthropic just made the case for MCP gateways: the custom glue is where the bugs live. For the MCP plane the glue is the OAuth server, audience validation, resource binding, and credential brokering, a fresh chance to get it wrong on every server you stand up. Bind the token once, broker the upstream credential once, and every server behind the boundary inherits the property instead of re-earning it.
This is the same boundary doing different work depending on which way the threat flows. Token binding stops a credential from being replayed across servers; response inspection stops a poisoned payload coming back from a tool, which is why injection in MCP flows backwards. The deterministic half, audience-bound tokens and no passthrough, is a guarantee, not a detection rate. We run our own team’s access to third-party MCP servers through this gateway, brokered through Auth0, so the tokens our agents hold are bound to one virtual server and the upstream keys never leave it.
If you would rather not own that stack of RFCs per server, the gateway owns it for you. The Zuplo MCP Gateway is in public beta. Spin up a Zuplo project and bind your first MCP server’s tokens to one resource today.
