← Back to blog
MCP·August 5, 2026·9 min read

MCP Authorization: How OAuth 2.1 Actually Secures a Remote MCP Server

MCP started as a trusted local subprocess with no auth story at all. Its authorization spec bolts on OAuth 2.1, PKCE, and resource indicators — here is what each piece actually prevents, including the token-passthrough bug that keeps showing up in early implementations.

Anyone who has wired an MCP server to a remote deployment has hit the same wall: the local, single-user story — spawn a subprocess over stdio, trust whoever's running the process — has no answer for "this server is reachable over HTTPS by many different agents, on behalf of many different users, and needs to know who's asking." Bolting on a static API key gets you something that works in a demo and fails an audit. MCP's authorization spec exists because that gap is real, and it's worth understanding the mechanics rather than treating "add OAuth" as a checkbox.

This post walks through what the spec actually requires, why it borrows so heavily from existing OAuth 2.1 machinery instead of inventing something bespoke, and where the sharpest edge case — token passthrough — actually bites.

Why stdio Auth and HTTP Auth Are Different Problems

The original MCP transport is a local subprocess talking JSON-RPC over stdin/stdout. There's no network hop, so there's no meaningful authentication question: whoever can launch the process already has whatever access the process has. Utilix's own MCP server, which exposes tools like Base64 encoding or JSON validation to an agent over stdio, works exactly this way — trust is inherited from the OS process boundary, and that's sufficient for a local dev tool.

Remote MCP servers over Streamable HTTP break that assumption completely. The server is now a shared endpoint. It has no OS-level signal about who's connecting — just an HTTP request. Without an authorization layer, "remote MCP server" is functionally an unauthenticated RPC endpoint that happens to speak a nice protocol, which is a bad place to expose anything that touches real data or paid APIs.

The spec's answer isn't a new auth scheme. It's a specific, opinionated profile of OAuth 2.1 — one that pins down the parts OAuth traditionally leaves as implementation choices, because an autonomous agent can't be expected to make the same judgment calls a human clicking through a login screen would.

Three Roles, Not Two

Standard OAuth tutorials talk about a client and a server. MCP's profile is explicit about three roles, because collapsing them is where implementations go wrong:

Separating resource server from authorization server is what lets an MCP server operator plug into whatever auth infrastructure a company already has, instead of forcing every MCP server to become its own identity provider.

Discovery: the client doesn't hardcode anything

An agent that wants to talk to an MCP server it's never seen before has no prior knowledge of which authorization server to use, what scopes are valid, or what the token endpoint is. The spec solves this with two metadata documents, both served over well-known URIs:

GET /.well-known/oauth-protected-resource
→ { "resource": "https://mcp.example.com",
    "authorization_servers": ["https://auth.example.com"],
    "scopes_supported": ["mcp:tools:read", "mcp:tools:call"] }

GET https://auth.example.com/.well-known/oauth-authorization-server
→ { "authorization_endpoint": "...", "token_endpoint": "...",
    "registration_endpoint": "...", "code_challenge_methods_supported": ["S256"] }

The first (RFC 9728, Protected Resource Metadata) tells the client which authorization server is authoritative for this MCP server. The second (RFC 8414) is the standard OAuth authorization server metadata document, telling the client where to send the actual auth requests. When an MCP client hits a protected endpoint without a token, the 401 response's WWW-Authenticate header points it at the first document — that's the entire bootstrap sequence, no out-of-band configuration required.

PKCE isn't optional here

In classic OAuth 2.0, PKCE (Proof Key for Code Exchange) was originally a mobile-app mitigation for authorization code interception. OAuth 2.1 folded it in as mandatory for all clients, and MCP inherits that without exception — every authorization code flow between an agent and an MCP authorization server must use PKCE, full stop. Given that MCP clients are frequently CLI tools, desktop agents, or other contexts that can't reliably keep a client secret confidential, treating every client as effectively "public" and requiring PKCE is the only sane default.

Dynamic Client Registration: Agents Can't Pre-Register Everywhere

A web app registers as an OAuth client once, by hand, in a developer console, and hardcodes the resulting client_id. That model doesn't scale to MCP, where an agent might discover and connect to an MCP server it has never talked to before, run by an operator the agent's developer has never contacted.

RFC 7591 Dynamic Client Registration solves this: the MCP client POSTs to the authorization server's registration endpoint, describing itself (redirect URIs, client name, grant types), and gets back a client_id on the spot — no human in the loop.

POST /register HTTP/1.1
Content-Type: application/json

{ "client_name": "My Agent",
  "redirect_uris": ["http://localhost:33418/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none" }

201 Created
{ "client_id": "8f3a...", "client_id_issued_at": 1735689600 }

DCR is listed as should support, not strictly mandatory, because an authorization server operator might reasonably want to keep registration behind manual approval for security reasons. But for the general "any agent should be able to connect to any compliant MCP server" story to work at all, DCR is effectively load-bearing. Without it, every MCP integration reverts to manual credential provisioning, which is exactly the friction OAuth was supposed to remove.

The Confused Deputy: Why Tokens Need an Audience

Here's the part of the spec that actually matters for security, not just plumbing: resource indicators.

Imagine an MCP server that, to do its job, calls out to a downstream API — a calendar API, a ticketing system, whatever. The naive implementation takes the bearer token the agent sent, and forwards that same token to the downstream API. This is the token passthrough anti-pattern, and it's a textbook confused-deputy vulnerability: the downstream API now has to trust a token it never issued, minted by an authorization server it may not even recognize, and any scope or audience restriction the original authorization server intended is gone. Worse, if that same access token is valid at multiple resource servers, a malicious or compromised MCP server can replay a user's token somewhere the user never authorized.

RFC 8707 Resource Indicators closes this by giving the client a way to tell the authorization server, at request time, exactly which resource server a token is for:

GET /authorize?...&resource=https://mcp.example.com
POST /token   body includes: resource=https://mcp.example.com

The authorization server mints a token whose audience is bound to that specific MCP server. A resource server that receives a bearer token is expected to validate that the audience claim actually names it — not silently accept any validly-signed token regardless of who it was minted for. And critically, an MCP server that needs to call a downstream API on the user's behalf should not just relay the inbound token; it should either hold its own separately-scoped credential for that downstream call, or go through its own token exchange, so the two hops never share a bearer token. The diagram below traces the full flow, with the point where audience binding and non-forwarding matter called out.

Agent (MCP Client)

Authorization Server

MCP Server (Resource Server)

Downstream API

1. MCP request, no token 2. 401 + WWW-Authenticate: resource_metadata=... 3. GET /.well-known/oauth-protected-resource 4. returns authorization_servers[] + scopes 5. GET /.well-known/oauth-authorization-server 6. POST /register (dynamic client registration) 7. client_id issued 8. /authorize + code_challenge + resource=mcp-server-url 9. redirect with authorization code 10. /token + code_verifier + resource=mcp-server-url 11. access_token, audience-bound to MCP server 12. MCP request, Authorization: Bearer <token> 13. new, separately-scoped token — never the client's 14. downstream response 15. tool result

What This Actually Changes for Builders

If you're standing up a remote MCP server, the spec gives you a short, concrete checklist rather than a philosophy:

  1. Don't invent your own auth. Point the resource metadata document at a real authorization server — your existing IdP if you have one — rather than building bespoke token issuance.
  2. Validate audience on every request. A bearer token that's valid but wasn't issued for your server is not a request you should honor, even if the signature checks out.
  3. Never forward the client's token to a third service. If your MCP server needs to call another API, that's a second, separately-scoped credential, not a relay.
  4. Support the metadata discovery documents. They're what let an agent connect to a server it's never seen configured for, which is most of the point of building on an open protocol in the first place.

None of this is unique to MCP — it's standard OAuth 2.1 resource-server hygiene. What MCP's spec did was make the ambiguous parts of that hygiene non-optional for a protocol where the "user" clicking through consent screens is increasingly an agent acting semi-autonomously, and where the cost of a confused-deputy bug is an agent leaking a credential to a server it was never meant to talk to.

Takeaway: if your MCP server's auth story is "check for an API key in a header," it'll work until someone points a second MCP server at it, or until a downstream API call needs its own credential — at which point the token-passthrough trap is exactly the bug the resource-indicators requirement exists to prevent. Build the audience check in from the start; retrofitting it after a server is already load-bearing for real integrations is the harder path.

#mcp#oauth#model-context-protocol#ai-agents#security#authorization

Related reading

MCP
What Actually Happens Inside an MCP Tool Call
Security
JWK Rotation in Production: What Actually Breaks When You Rotate a Signing Key
Prompt Engineering
Prompt Engineering for Agents Is a Different Discipline Than Prompt Engineering for Chat