Your MCP Server Must Refuse That Token

You are wrapping a third-party API in an MCP server. The client that connects to you already holds an access token for that API, and it sends the token along. Your server has the token, the API wants the token, and there is a perfectly good Authorization header sitting right there.
Forward it. Ship it. Move on.
The MCP specification says otherwise, in the flattest language it uses anywhere: MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server. Most of the security guidance in MCP is a SHOULD. This one is a MUST NOT, and it is worth understanding why the bar is set that high.
In the shortcut, token A is the one the client already holds, issued by the third-party API's own authorization server:
The second arrow is what the specification forbids: your server presents a credential issued to somebody else.
This describes the security best practices and authorization sections of the 2026-07-28 specification, the current revision. The prohibition has been in the spec since the 2025-06-18 revision, which added audience validation and the Security Best Practices document, and it did not change in this one. What did change is the surrounding machinery, noted at the end. Code here is structural, meant to show shape instead of to be pasted into production.
What "token passthrough" actually names
The anti-pattern has two halves, and they fail independently.
Half one: accepting a token that was issued for somebody else. A JWT carries an aud (audience) claim naming the resource it was minted for. If your server reads the token, finds a valid signature from an issuer it recognises, and starts serving requests without checking that aud names your server, then any token from that issuer opens your server. A token a user granted to an unrelated application now works against yours.
Half two: forwarding that token onward. Your server takes the credential it was handed and replays it, unchanged, against the downstream API. The API sees a request that looks like it came from the original client.
Half one is the audience validation failure. Half two is the passthrough. A server can commit the first without the second, and it is still broken.
Four reasons the spec makes this a MUST NOT
The specification lists the risks under four headings, and they are worth reading as four separate arguments instead of one:
| The spec's heading | What stops working |
|---|---|
| Security Control Circumvention | controls that depend on who the token was issued to |
| Accountability and Audit Trail Issues | logs on both sides that name the wrong caller |
| Trust Boundary Issues | the downstream API's assumption about who calls it |
| Future Compatibility Risk | the controls you will want to add later |
- Security controls stop applying. Rate limits, request validation and traffic monitoring are usually keyed to the audience of a credential or to some other constraint on it. A token that reaches the downstream API without your server checking whether it was meant for you has walked around whichever of those controls depended on knowing who was calling.
- The audit trail stops being true. Your server cannot distinguish one MCP client from another when they all arrive holding upstream-issued tokens that may be opaque to you. Downstream, the API's logs show requests that appear to come from a different identity than the one actually forwarding them. When you come to investigate an incident, the logs point at the wrong party, which is more expensive to unpick than a gap in the logs would have been.
- Trust boundaries move without anyone deciding to move them. The downstream API granted trust to a specific party, with assumptions about who calls it and how. If a token is accepted by several services without proper validation, an attacker who compromises one of them can use the same token against every other service that accepts it.
- You are choosing today which security model you can have later. A server that starts as a pure proxy usually acquires controls of its own eventually. Starting with proper audience separation is what makes that possible without a migration.
How it becomes the confused deputy
The passthrough half is the ingredient in a larger attack the spec documents separately.
A confused deputy is a component with more authority than its caller, tricked into using that authority on the caller's behalf. An MCP proxy server that holds a static client id with a third-party authorization server is exactly such a deputy.
The attack starts from a legitimate authorisation: the user approves the proxy once, and the third-party authorization server sets a consent cookie for that static client id. The attacker's steps follow:
The skipped consent screen is the step that matters: the third-party authorization server treats the attacker's link as the approval the user already gave.
A server that forwards tokens without validating them is already living in that world. It has accepted that a credential's audience is somebody else's problem, and audience is the mechanism that would otherwise have refused.
The mitigations the spec requires of a proxy server are worth knowing even if you do not build one:
- Per-client consent, stored server side and checked before the third-party flow begins.
- Exact string matching on redirect URIs, with patterns and wildcards excluded.
- Single-use
statevalues, stored only after consent has been approved.stateis the OAuth parameter that ties a callback back to the request that started it. - Consent cookies with the
__Host-prefix, plusSecure,HttpOnlyandSameSite=Lax. The prefix binds the cookie to one origin.
What to do instead
The shape that works has three steps, and none of them is difficult once you stop thinking of the token as cargo. Two tokens travel this path:
Token B is the only credential the third-party API sees, and it names your server as the party that asked.
-
Be a resource server, and act like one. Your MCP server is an OAuth 2.1 resource server: the party that holds the protected data and accepts access tokens for it. It publishes protected resource metadata, it challenges unauthenticated requests with the
WWW-Authenticateheader, and it validates every token it accepts: signature, issuer, expiry, and audience. The audience check is the one people skip, and it is the one this article is about. -
Get your own credential for anything downstream. If your server calls a third-party API, it needs a credential issued to it, and which one depends on the downstream provider. An OAuth token exchange (RFC 8693) takes the caller's token and swaps it for one whose audience is the downstream API. A client credentials grant gets your server a token on its own account. A per-user credential is one your server stores after its own authorization flow with that provider. In every case the credential your server presents downstream is one issued to your server, and the inbound token stays where it arrived.
-
Ask for the narrowest scopes that will do. The spec's scope minimisation guidance is a direct consequence of the same reasoning: a stolen broad token is a worse day than a stolen narrow one. Start from a minimal set, and elevate through targeted
WWW-Authenticatechallenges when a privileged operation is first attempted.
The audience check, in shape
In a Spring Boot resource server, the audience check is a validator you add alongside the default ones. The important part is what it asserts, not the API surface:
JwtDecoder decoder(String issuer, String thisServer) {
NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer);
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer(issuer),
new JwtClaimValidator<List<String>>("aud",
aud -> aud != null && aud.contains(thisServer))));
return decoder;
}
JwtValidators.createDefaultWithIssuer gives you expiry, not-before and issuer. It does not give you audience, and that is the gap this closes. A token minted for another service, signed by an issuer you trust, is rejected here instead of being served.
Three things worth stating plainly about the code above. It is structural: check it against the Spring Security version you are on before you rely on it. The value you compare against is your server's own resource identifier, the same one you publish in your protected resource metadata, so that a client asking its authorization server for a token to call you names you specifically.
Here is what the defaults cover, and what they leave to you:
| Claim or header | createDefaultWithIssuer | What you still add |
|---|---|---|
exp and nbf | checked | already covered |
iss | checked | already covered |
typ | must be JWT or absent | acceptance of at+jwt, when your authorization server issues RFC 9068 tokens |
aud | left to you | the audience check in the code above |
The typ row is the second gap in the defaults, and it catches people out. RFC 9068 access tokens, the profile the MCP spec points at when it talks about audience claims, use typ: at+jwt. If your authorization server issues those, the default chain rejects them whatever your audience check decides, and the symptom is a 401 that looks like a signature problem. Add JwtTypeValidator for at+jwt when that applies to you, or start from JwtValidators.createAtJwtValidator(), which Spring Security 6.5 added to build the whole RFC 9068 chain.
What changed in 2026-07-28 around this
The prohibition is old. The machinery around it moved in the current revision, and two changes touch this directly.
- Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents, where a client identifies itself with an HTTPS URL that serves its own metadata. Dynamic registration is one of the four conditions the confused deputy attack needs, so this is a change with a security reason behind it. It keeps working for authorization servers that cannot do CIMD yet.
- Clients must validate
isson authorization responses when one is present (RFC 9207), against the issuer they recorded before redirecting. This mitigates mix-up attacks, where an attacker-controlled authorization server persuades a client to send it a code issued by an honest one. PKCE, which makes a client prove that the code it redeems is the one it asked for, does not stop that on its own, because the client hands itscode_verifierto whichever token endpoint it was steered towards.
Three questions for your server
- Does your token validation assert the
audclaim against your own identifier? If the answer is "the signature and the issuer check out", that is a no. - Does any code path put an inbound
Authorizationheader on an outbound request? Search for it. That is the passthrough, and it usually looks like tidy code reuse. - If someone hands your server a valid token that was minted for a different service, what happens? The correct answer is a
401with aWWW-Authenticatechallenge.
The difficulty with this one is that getting it wrong feels fine. Every request succeeds, every integration test passes, and the problem stays invisible until somebody points a token at your server that you should have refused.
If you are building remote servers, MCP Just Went Stateless covers the other half of this, including State Handle Hijacking, which is the same lesson applied to the handles that replaced sessions. The Security Model module of MCP Fundamentals works through the trust boundaries from first principles.
Further Reading
- MCP specification: Security Best Practices: the MUST NOT itself, the four risks behind it, and the confused deputy attack with the full list of proxy mitigations.
- MCP specification: Authorization: the resource server role, the audience requirement, and the table of what a client must validate on an authorization response.
- MCP specification: Authorization Security Considerations: the rest of the authorization security guidance, including localhost redirect URI risks and the trust policies an authorization server can apply to client metadata documents.
- MCP specification: Client Registration: why Dynamic Client Registration was deprecated, and what a Client ID Metadata Document has to contain.
- RFC 9068: JWT Profile for OAuth 2.0 Access Tokens: the
at+jwttype header, and the validation steps a resource server owes an access token. - RFC 8693: OAuth 2.0 Token Exchange: how to swap the caller's token for one whose audience is the downstream API.
- RFC 9700: Best Current Practice for OAuth 2.0 Security: the OAuth security guidance the MCP specification tells implementers to read alongside it.
- Spring Security: OAuth 2.0 Resource Server JWT: the reference version of the audience validator, with the decoder wiring this article sketches.
Sources
- MCP specification 2026-07-28: Security Best Practices: the exact MUST NOT wording, the two halves of the anti-pattern, and the four named risks. It also carries the four conditions the confused deputy attack needs, the step by step description, the proxy mitigations, and the scope minimisation guidance.
- MCP specification 2026-07-28: Authorization: that an MCP server acts as an OAuth 2.1 resource server, that it must validate the audience, and that an invalid token gets a
401withWWW-Authenticate. - MCP specification 2026-07-28: Authorization Security Considerations: that the upstream credential is a separate token, and that the server must not pass through the one it received.
- MCP specification 2026-07-28: Client Registration: that Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents, and stays available for authorization servers that lack them.
- MCP specification 2026-07-28: Key Changes: that the deprecation and the RFC 9207
issrequirement are both changes made in this revision. - MCP specification: Versioning: that
2026-07-28is the current protocol revision. - MCP specification 2025-06-18: Authorization: the revision the prohibition entered, with "MCP servers MUST NOT accept or transit any other tokens".
- RFC 9068: JWT Profile for OAuth 2.0 Access Tokens: that these access tokens carry
typ: at+jwt. - RFC 8693: OAuth 2.0 Token Exchange: the token exchange named in step 2.
- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification: the
issparameter, and the mix-up attack it mitigates. - RFC 9728: OAuth 2.0 Protected Resource Metadata: the document in which a resource server publishes the identifier you compare
audagainst. - Spring Security API: JwtValidators: that
createAtJwtValidator()builds the RFC 9068 chain and arrived in 6.5. - Spring Security API: JwtTypeValidator: that
jwt()requires thetypheader to beJWTor absent. - Spring Security: OAuth 2.0 Resource Server JWT: that the defaults verify
iss,expandnbfand leaveaudto you, and the decoder wiring in the code block,JwtDecoders.fromIssuerLocationthroughsetJwtValidator.