MCP authorization reference

MCP token passthrough and the confused deputy

Why forwarding a client's token to an upstream API is explicitly disallowed by the MCP specification, what breaks when you do it, and the exchange pattern that replaces it.

Updated July 2026Implementation guidemcp token passthrough
Built for

Engineers building MCP servers that call third-party APIs on a user's behalf.

Decision supported

How an MCP server obtains upstream credentials without relaying the token it was given.

The control gap

Token passthrough is the shortcut every MCP server reaches for first. The client sends a bearer token, the server puts that same token on the outbound call to GitHub, Slack, or an internal API, and it works. It works because the token was never audience-bound, which is exactly the problem. The upstream service now sees a request it cannot attribute to the MCP server, your rate limits and audit trail belong to someone else, and any server that receives the token can replay it anywhere the token is accepted.

What good looks like

The token presented to the MCP server is validated for that server only, and upstream access uses a separate credential the server obtained in its own right, with the delegated user recorded on both sides of the boundary.

  • Reject any token whose audience is not this MCP endpoint, even when the signature is valid and the issuer is trusted.
  • Obtain upstream credentials through the server's own authorization relationship, not by reusing the inbound token.
  • Carry the delegated user as decision context rather than as a credential, so policy can see who the action is for without the identity becoming a bearer secret.
  • Never accept a token the client did not obtain for this resource, which is how the confused deputy arises when one server proxies for many.

A production workflow

  1. Validate the inbound token: issuer, signature, expiry, scope, and audience matching this endpoint.
  2. Resolve the delegated user and the requested tool and resource from the request, not from the token alone.
  3. Evaluate policy for that agent, that user, that action, and that target before touching anything upstream.
  4. Execute with the server's own upstream credential, and attach the upstream result to the decision record that authorised it.

Copy this

The difference is one line of code and a completely different trust model. The first version makes every downstream service trust whoever holds the client's token.

# Do not do this: the inbound token becomes an upstream credential
inbound = request.headers["Authorization"]        # Bearer <client token>
upstream = http.post(GITHUB_API, headers={"Authorization": inbound})

# Do this: validate inbound for THIS resource, then act with your own credential
claims = verify(inbound_token,
                issuer=AUTH_SERVER,
                audience="https://endram.com/mcp/github-tools")   # audience check
decision = authorize(agent=claims.client_id,
                     delegated_by=claims.sub,
                     tool="github.merge_pull_request",
                     resource="acme/payments#4192")
if decision.effect != "allow":
    return decision.as_mcp_error()
upstream = http.post(GITHUB_API, headers={"Authorization": server_credential()})

# The record that survives the request
{ "agent": "...", "delegated_by": "...", "tool": "...", "resource": "...",
  "decision": "allow", "policy_version": 14, "upstream_status": 200 }

The second form costs one extra credential and gives you attribution on both sides. When something unexpected happens upstream, you can name the agent, the human, the policy version, and the exact arguments rather than inferring them from an access log.

Evidence to require

  • Rejected tokens whose audience named a different resource.
  • Which upstream credential executed each call, distinct from the identity that requested it.
  • The delegated user recorded as decision context on every upstream action.
  • Any attempt to present a token obtained for a different MCP server on this endpoint.

Buyer checklist

  • Does the server ever place an inbound Authorization header on an outbound request?
  • Can the upstream service tell which MCP server called it, or only which user's token was used?
  • If a downstream service is compromised, does it hold credentials that work anywhere else?
  • Are delegated identity and bearer authority separated in the code, or the same value?

Practical answers

Common implementation questions

Is token passthrough always wrong?

It is disallowed by the MCP authorization specification, and the reason is practical rather than doctrinal. A relayed token defeats audience binding, removes attribution, and turns each server into a credential distribution point for every other.

What replaces it when the upstream really does need user context?

Send the user as context, not as authority. Where the upstream supports a delegation grant, use token exchange to mint a downstream token whose audience is the upstream service and whose subject is the user.

How does Endram avoid it?

Endram validates the inbound token against the gateway's own canonical resource, evaluates the tool call, and forwards using the upstream credential registered for that gateway. The inbound token never leaves the boundary.

Continue the evaluation

Related controls