Zuplo
Model Context Protocol

Bind Every MCP Token to One Server

Martyn DaviesMartyn Davies
June 12, 2026
7 min read

Two MCP servers behind one identity provider, an issuer-only check, and a token minted for one walks into the other. Audience binding pins each token to a single server.

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.

Use this approach if you're:
  • 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.

http
# Authorization and token requests carry the canonical URI of the
# one MCP server this token is for. The AS binds it into the audience.
&resource=https%3A%2F%2Fgateway.example.com%2Fmcp%2Forders

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:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://gateway.example.com/.well-known/oauth-protected-resource/mcp/orders"

The client fetches that document to learn which authorization server to talk to and which resource to name:

JSONjson
// GET /.well-known/oauth-protected-resource/mcp/orders
{
  "resource": "https://gateway.example.com/mcp/orders",
  "authorization_servers": ["https://gateway.example.com"]
}

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

The Zuplo MCP Gateway acts as both OAuth Authorization Server and Resource Server: an agent's token bound to /mcp/orders is accepted there and rejected at /mcp/billing on an audience mismatch, while the gateway reaches the upstream Orders API with a separately brokered credential the client never holds.

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:

StandardWhat the gateway handles
Dynamic Client Registration (RFC 7591)Clients register themselves, no manual app setup per client
PKCE with S256Required 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 providerPolicy preset (links to its setup guide)
Auth0mcp-auth0-oauth-inbound
Oktamcp-okta-oauth-inbound
Microsoft Entra IDmcp-entra-oauth-inbound
Googlemcp-google-oauth-inbound
Any OIDC issuermcp-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 Zuplo portal policy editor showing the auth0-managed-oauth policy (MCP Auth0 OAuth), with module @zuplo/runtime/mcp-gateway, export McpAuth0OAuthInboundPolicy, and auth0Domain, clientId, and clientSecret each sourced from an environment variable.

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.