Skip to main content

Securing MCP Servers: OAuth, Audience Binding & the Confused Deputy

Advanced
What you'll learn
  • 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:

Who is who in an MCP OAuth flow
Press Enter or Space to flip the card. Use the left and right arrow keys to move between cards.Term shown.
1 / 3

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:

Guided walkthrough1 of 6
  1. 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.

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

Guided walkthrough1 of 3
  1. 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.

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.

Watch out
  • 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:

Guided walkthrough1 of 7
  1. All AS endpoints MUST be HTTPS. Redirect URIs MUST be HTTPS or localhost โ€” nothing else.

Check yourselfโ€‹

Check yourself

0/4
  1. A remote MCP server receives a request with no access token. What does the spec require it to do first?
  2. What is token audience binding (RFC 8707) protecting against?
  3. Your MCP server needs to call an upstream GitHub API. What should it do with the access token the client sent it?
  4. For a STDIO (local) MCP server, how does the spec say credentials should be handled?

Sources & further readingโ€‹