---
title: "Bind Every MCP Token to One Server"
description: "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."
canonicalUrl: "https://zuplo.com/blog/2026/06/12/bind-mcp-tokens-to-one-server"
pageType: "blog"
date: "2026-06-12"
authors: "martyn"
tags: "Model Context Protocol, API Security"
image: "https://zuplo.com/og?text=Bind%20Every%20MCP%20Token%20to%20One%20Server"
---
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](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization):
"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.

<CalloutAudience
  variant="useIf"
  items={[
    `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](https://modelcontextprotocol.io/specification/2025-11-25/basic/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](https://datatracker.ietf.org/doc/html/rfc8707)
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](https://datatracker.ietf.org/doc/html/rfc9728):

```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:

```json
// 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.](/blog-images/2026-06-12-bind-mcp-tokens-to-one-server/diagram-1.png)

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](/blog/introducing-zuplo-mcp-gateway), in public beta, does
it at the boundary. For its `/mcp/{routePath}` routes, per the
[auth overview](https://zuplo.com/docs/mcp-gateway/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.

<CalloutDoc
  title="MCP Gateway Auth Overview"
  description="How the gateway acts as OAuth 2.1 AS and RS, binds tokens with RFC 8707 resource indicators, and brokers upstream credentials without passthrough."
  href="https://zuplo.com/docs/mcp-gateway/auth/overview"
  icon="book"
/>

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`](https://zuplo.com/docs/mcp-gateway/auth/configuring-auth0)   |
| Okta               | [`mcp-okta-oauth-inbound`](https://zuplo.com/docs/mcp-gateway/auth/configuring-okta)     |
| Microsoft Entra ID | [`mcp-entra-oauth-inbound`](https://zuplo.com/docs/mcp-gateway/auth/configuring-entra)   |
| Google             | [`mcp-google-oauth-inbound`](https://zuplo.com/docs/mcp-gateway/auth/configuring-google) |
| Any OIDC issuer    | [`mcp-oauth-inbound`](https://zuplo.com/docs/mcp-gateway/auth/configuring-generic-oidc)  |

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.](/blog-images/2026-06-12-bind-mcp-tokens-to-one-server/policy-editor.png)

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.

<CalloutTip variant="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.
</CalloutTip>

## 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](/blog/anthropic-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](/blog/protect-mcp-against-prompt-injection).
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](https://portal.zuplo.com/signup?utm_source=zuplo-blog&utm_medium=web&utm_campaign=mcp-gateway)
and bind your first MCP server's tokens to one resource today.