Security

MCP token passthrough and the confused deputy problem

The 2026-07-28 MCP spec forbids passing a client's token through to upstream APIs and requires audience-bound tokens. Here is the anti-pattern and the fix.

Mark

Head of Marketing, MCPOrbit

Published
Updated
· Updated
Read time
· 10 min read
Diagram of an MCP server rejecting a client token forwarded to an upstream API, and minting its own audience-bound token instead.

An MCP server must never forward the token it received from a client to an upstream API. The 2026-07-28 Model Context Protocol (MCP) spec calls this token passthrough and forbids it. Your server accepts only tokens minted for its own audience, and it uses its own credentials to call anything downstream.

This one rule closes two real holes. Token passthrough lets a token meant for your server be replayed against a service it was never scoped for. The confused deputy problem lets an attacker ride your server's existing consent at an upstream authorization server to steal an auth code. Both come from the same mistake: treating the client's token as a universal key. Here is what the spec requires, the code that enforces it, and the upstream-call pattern that replaces passthrough.

What is token passthrough, and why does the MCP spec forbid it?

Token passthrough is when an MCP server takes the Authorization header it received and reuses that same token to call a different service. It looks convenient. The client already sent a valid token, so why mint another? The problem is audience. An OAuth 2.1 access token is issued for one resource, named in its aud claim under RFC 8707 resource indicators. A token your server accepts was scoped for your server. Forwarding it to an upstream API asks that API to trust a credential it never issued.

When servers pass tokens through, three guarantees break. Audience validation stops meaning anything, because tokens flow to resources they were never issued for. Rate limits and abuse controls on the upstream API attribute traffic to the wrong party. And a single leaked token now reaches every service in the chain instead of one. The spec is blunt for a reason: an MCP server must not accept tokens that were not issued for it, and must not forward client tokens to upstream services.

// ANTI-PATTERN: do not do this.
// Forwarding the client's token to an upstream API is token passthrough.
app.post("/mcp", async (req, res) => {
  const clientToken = req.headers.authorization  // token issued for THIS server
  const upstream = await fetch("https://api.example.com/data", {
    headers: { authorization: clientToken },      // replayed at the wrong audience
  })
  const data = await upstream.json()
  // the upstream API now trusts a token it never issued
})

How do you validate the token audience?

The fix starts at the door. Before your server runs any tool, it validates the incoming token and checks that the aud claim equals your server's resource identifier. If it does not match, reject with 401. This is the single control that makes token passthrough impossible to do by accident, because a token scoped for another service fails the check.

// server: validate every incoming token before running a tool.
// [email protected], Node 25.
import { createRemoteJWKSet, jwtVerify } from "jose"

const RESOURCE = "https://mcp.example.com"        // this server's identifier
const jwks = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json"),
)

export async function requireToken(authHeader) {
  const token = authHeader?.replace(/^Bearer /i, "")
  if (!token) throw new HttpError(401, "missing token")

  const { payload } = await jwtVerify(token, jwks, {
    issuer: "https://auth.example.com",
    audience: RESOURCE,                            // RFC 8707: token must be for us
  })
  return payload                                   // sub, scope, and the rest
}

jwtVerify throws if the signature, issuer, expiry, or audience is wrong. Passing audience: RESOURCE is the line that enforces the spec. A token whose aud names a different service never reaches your tools.

How should an MCP server call an upstream API without passthrough?

Your server needs its own credential for the upstream service. It should never borrow the client's. There are two clean patterns, and both keep the client's token bound to your server only.

Pattern 1: the server's own client credentials

If the upstream call is not on behalf of a specific user, use the OAuth 2.1 client credentials grant. Your server authenticates as itself and gets a token whose audience is the upstream API.

// server-to-service: the server gets its OWN token for the upstream API.
async function upstreamToken() {
  const res = await fetch("https://auth.example.com/token", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.UPSTREAM_CLIENT_ID,
      client_secret: process.env.UPSTREAM_CLIENT_SECRET,
      resource: "https://api.example.com",         // audience is the upstream, not us
    }),
  })
  const { access_token } = await res.json()
  return access_token
}

Pattern 2: token exchange for a user-bound call

If the upstream call must act as the user, exchange the incoming token for a new one using OAuth token exchange (RFC 8693). Your authorization server issues a fresh token whose audience is the upstream API and whose subject is the same user. The client's original token still never leaves your server.

// RFC 8693 token exchange: swap our audience-bound token for an upstream one.
async function exchangeForUpstream(subjectToken) {
  const res = await fetch("https://auth.example.com/token", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
      subject_token: subjectToken,
      subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
      resource: "https://api.example.com",         // the new audience
    }),
  })
  const { access_token } = await res.json()
  return access_token                              // audience is the upstream API
}

Both patterns share one property: the token that reaches the upstream API was issued for that API. Audience validation on the upstream side now works, and a token leaked from your server cannot be replayed anywhere else.

What is the confused deputy problem in an MCP server?

The confused deputy problem shows up when your MCP server acts as a proxy in front of a third-party authorization server, using a single static client ID. The server is the deputy. It holds authority, an existing consent grant, that an attacker tricks it into using on their behalf.

Here is the attack. Your server is registered with an upstream authorization server under one static client ID. A user consented to that client once, so the upstream server set a consent cookie and stopped showing the approval screen for it. An attacker sends the victim a crafted authorization link that points at the upstream server, names your static client ID, and sets a redirect_uri the attacker controls. Because the consent cookie is already present, the upstream server skips the prompt and redirects the auth code straight to the attacker. The attacker exchanges it and now holds a token issued through your server's identity.

How do you prevent the confused deputy attack?

The spec's rule is direct. An MCP proxy server that uses a static client ID must obtain the user's consent for each dynamically registered client before it forwards the request to the third-party authorization server. Do not rely on the upstream consent cookie. Prompt on your own side, every time a new client is involved.

  • Require your own consent screen for each downstream client, even when the upstream server would skip its prompt.
  • Validate `redirect_uri` against an exact allow-list. Never reflect a caller-supplied redirect without checking it.
  • Prefer per-client identity over one shared static client ID. Client ID Metadata Documents (CIMD) in the 2026-07-28 spec give each client its own verifiable identity.
  • Bind the authorization request to the specific client with PKCE and a `state` value you generate and check.

The last point connects to the rest of your auth stack. If you already moved off Dynamic Client Registration to CIMD, each client carries its own identity document, so the single-static-client trap does not apply. Audience-bound tokens and per-client consent are the two controls that close token passthrough and the confused deputy at once.


A short checklist before you ship

  • Every incoming token is verified for signature, issuer, expiry, and an `aud` that equals this server.
  • No code path forwards the client's `Authorization` header to another service.
  • Upstream calls use client credentials or RFC 8693 token exchange, with the upstream API as the audience.
  • Proxy flows require your own per-client consent and validate every `redirect_uri`.

Frequently asked questions

What is token passthrough in MCP?
Token passthrough is when an MCP server reuses the access token a client sent it to call a different upstream service. The 2026-07-28 MCP spec forbids it. A token is issued for one audience, so forwarding it asks another service to trust a credential it never issued.
Why can't my MCP server forward the client's token to an upstream API?
Because the token's aud claim names your server, not the upstream API. Forwarding it breaks audience validation and lets one leaked token reach every service in the chain. Get a separate token for the upstream call using client credentials or RFC 8693 token exchange.
What is the confused deputy problem in MCP?
It is an attack on an MCP server that proxies a third-party authorization server with a static client ID. An attacker reuses the server's existing consent to get an auth code redirected to themselves. The fix is to require fresh user consent for each client and validate every redirect_uri.
How do I validate an MCP access token?
Verify the token's signature against the issuer's JWKS, then check iss, exp, and that aud equals your server's resource identifier. A library like jose does all four in one jwtVerify call. Reject with 401 if any check fails.
Does moving to CIMD stop the confused deputy problem?
It helps. Client ID Metadata Documents give each client its own verifiable identity, so you are not relying on one shared static client ID that a forged request can borrow. You still need per-client consent and strict redirect_uri checks.
Is token passthrough ever allowed?
No. The spec states an MCP server must not accept tokens that were not issued for it. If you need to act on an upstream service, obtain a token whose audience is that service. There is no compliant shortcut that forwards the original token.

About the author

Mark

Head of Marketing, MCPOrbit

Mark leads marketing at MCPOrbit and writes the MCP security and build-it guides, checked against the spec before they ship.

Share this post

MCPOrbit

Test an MCP server in 60 seconds.

Download MCPOrbit for free — no account, no telemetry. Hear about a server and test it before the curiosity wears off.

macOS 14+ · Apple Silicon & Intel · No account needed