Securing MCP Servers: OAuth, Audience Binding & the Confused Deputy
- Understand why a remote (HTTP) MCP server is an OAuth 2.1 resource server, not just an API key endpoint
- Trace the discovery handshake: 401 โ Protected Resource Metadata โ Authorization Server Metadata โ token
- Explain token audience binding (RFC 8707) and why it stops one service's token from working at another
- Name the confused-deputy trap and the one rule that closes it: never pass a client's token through to an upstream API
- Apply a short hardening checklist before you expose an MCP server to the internet
MCP went from novelty to the default way agents reach tools โ which means MCP servers now sit in front of real data and real actions. A local server you launch over STDIO trusts its environment: it reads credentials from env vars and there's no network boundary to defend. The moment you make that same server remote (HTTP), anyone who can reach the URL can try to call it. That flips it into an authorization problem, and the MCP spec answers with OAuth 2.1 โ not a bespoke API-key scheme.
This page is about the remote case. If your server is STDIO-only, the spec explicitly says don't follow the OAuth flow โ pull credentials from the environment and move on.
The three rolesโ
OAuth splits the problem into three parties. MCP maps onto them cleanly:
The key mental shift: the MCP server never handles the login itself. It only validates tokens someone else issued. That separation is what lets you put an off-the-shelf identity provider in front of a server you wrote.
The discovery handshakeโ
A client shouldn't need to be pre-configured with where to authenticate. MCP makes discovery automatic, driven by a 401:
- The very first request goes out bare. The server rejects it with HTTP 401 Unauthorized and a WWW-Authenticate header pointing at its resource-metadata URL.
- It GETs /.well-known/oauth-protected-resource on the server. The document's authorization_servers field names at least one Authorization Server the client can use.
- It GETs the AS's /.well-known/oauth-authorization-server to learn the authorize and token endpoints and supported capabilities.
- If the client has no client ID for this AS, it can POST /register to obtain one with no human in the loop โ crucial because a client can't know every MCP server in advance.
- The client generates a PKCE verifier/challenge, opens the browser to the authorize URL including the resource parameter, the user consents, and the client exchanges the returned code (with the verifier) for an access token.
- Now every request carries Authorization: Bearer <token>. The server validates it and responds.
Notice there's no hardcoded auth config on the client side โ the 401 bootstraps everything. That's the whole point: an agent can connect to a server it has never seen and figure out how to authenticate.
Audience binding: the load-bearing ruleโ
Here's the failure mode that audience binding exists to prevent. Say a user has a token issued for calendar.example.com. A malicious (or just sloppy) MCP server at evil.example.com tricks the client into sending that token to it. If evil accepts it, it can now turn around and call the calendar API as the user. One service's token worked at another. OAuth's security boundary just collapsed.
The fix is Resource Indicators (RFC 8707):
- On both the authorization request and the token request, the client MUST include a resource parameter set to the canonical URI of the MCP server it intends to call โ e.g. resource=https://mcp.example.com. It sends this even if it's unsure the AS supports it.
- When supported, the AS stamps the token so it is only valid for that specific resource server.
- Before doing any work, the MCP server MUST verify the token was issued for IT โ checking the audience claim (RFC 9068). A token minted for anyone else gets a 401, full stop.
Resource parameter on the authorization request (URL-encoded)
&resource=https%3A%2F%2Fmcp.example.com
Canonical URIs are strict: https://mcp.example.com and https://mcp.example.com:8443/mcp are valid; mcp.example.com (no scheme) and https://mcp.example.com#frag (fragment) are not. Prefer the form without a trailing slash for interoperability.
The confused deputy: never pass the token throughโ
This is the mistake that turns a well-meaning MCP server into an attacker's proxy. It's the same confused-deputy problem from agent security, sharpened to one concrete rule.
An MCP server often needs to call an upstream API (GitHub, a database service, another SaaS). The temptation is to take the token the client handed you and forward it upstream. Don't. The spec is blunt: the MCP server MUST NOT pass through the token it received from the client.
Why it's dangerous: the client's token was issued for your server as its audience. If you forward it, the upstream API may trust it as though it came from you, or assume you already validated it โ and now a token scoped for one hop is doing work two hops away, outside anyone's consent model.
- If your MCP server calls an upstream API, it acts as a SEPARATE OAuth client to that API and obtains its OWN token from the upstream authorization server. Two independent tokens, two independent audiences. The client's token stops at your door.
A pre-flight hardening checklistโ
Before a remote MCP server touches the public internet:
- All AS endpoints MUST be HTTPS. Redirect URIs MUST be HTTPS or localhost โ nothing else.
- Reject any token not issued specifically for this server. This is the single check that stops cross-service token reuse.
- Clients MUST use PKCE so an intercepted authorization code is useless without the matching verifier.
- The AS MUST match redirect URIs exactly against pre-registered values, and clients SHOULD use and verify the state parameter โ both defend against open-redirect phishing.
- Issue short-lived access tokens to limit the damage of a leak; for public clients, rotate refresh tokens. Store tokens securely and never log them.
- Tokens go in the Authorization header, never the query string, where they'd land in logs and referrers.
- Audience binding is the transport gate; still apply least privilege, sandboxing, and human-in-the-loop from /docs/security/securing-agents. Auth says WHO โ it doesn't say the request is safe.
Check yourselfโ
Check yourself
0/4Sources & further readingโ
- MCP Authorization specification (2025-06-18) โ the normative flow, roles, and MUST/SHOULD requirements this page summarizes.
- MCP Security Best Practices โ token passthrough, confused deputy, and why they're forbidden.
- RFC 8707 โ Resource Indicators for OAuth 2.0 โ the
resourceparameter and audience binding. - RFC 9728 โ OAuth 2.0 Protected Resource Metadata โ how a resource server advertises its authorization servers.
- RFC 8414 โ OAuth 2.0 Authorization Server Metadata and RFC 7591 โ Dynamic Client Registration.
- OAuth 2.1 draft โ PKCE, communication security, and token-handling requirements.
- Related on AILmanac: MCP Tool Poisoning, Rug Pulls & Agentjacking โ the other half of MCP security, at the description layer ยท Securing Agents & Tools ยท Prompt Injection ยท MCP in Claude Code.