OAuth 2.1 for MCP servers: PKCE, DCR and RFC 8707 in plain English
MCP authorization is OAuth 2.1 plus three RFCs that most OAuth tutorials never mention. This is the concept explainer — what each piece does and why the spec requires it. For the deployment how-to, see the hosting guide.
Updated July 2026
Why MCP needs more than classic OAuth
Classic OAuth assumes you know your clients in advance: you register an app in a dashboard, get a client_id and client_secret, and ship them inside your application. MCP breaks both assumptions. The client is someone's Claude Desktop or Cursor install — you will never pre-register it — and it runs on a desktop where a baked-in secret is not a secret. OAuth 2.1 plus three extension RFCs is how the MCP spec resolves this.
The two roles: Authorization Server and Resource Server
Every OAuth setup splits into two jobs. The Authorization Server (AS) authenticates users and issues tokens. The Resource Server (RS) — your MCP server — accepts requests only when they carry a valid token. They can be the same deployment or separate services; the protocol only cares that the RS can verify what the AS signs, typically via a published JWKS (the AS's public keys).
PKCE (RFC 7636): proof that the token requester started the flow
The authorization-code flow hands the client a one-time code via a browser redirect, and the client exchanges that code for a token. The classic attack is stealing the code in-flight and exchanging it yourself. PKCE closes this: the client invents a random secret (the verifier), sends only its hash (the challenge) when the flow starts, and must present the original verifier at exchange time. A thief who intercepted the code never saw the verifier, so the code is useless to them.
In OAuth 2.1, PKCE is mandatory for every client — the old exemption for "confidential" server-side clients is gone. If your AS treats PKCE as optional, it is not OAuth 2.1.
Dynamic Client Registration (RFC 7591): clients register themselves
When a user adds your MCP server to Claude Desktop, the client calls your AS's registration endpoint at runtime — "here is my name and redirect URI" — and receives a fresh client_id on the spot. No dashboard, no support ticket, no shared credentials between users. DCR is what makes "paste a URL, connect, done" possible: without it, every user of every MCP client would need you to manually provision app credentials.
Resource Indicators (RFC 8707): tokens bound to one server
A user might connect their client to ten different MCP servers. Without Resource Indicators, a token minted for server A could be replayed against server B — any server you talk to could impersonate you elsewhere. RFC 8707 has the client name the exact server (the resource) when requesting the token, and the AS bakes that audience into the token. Your MCP server then rejects any token whose audience is not itself. One token, one server, no cross-server replay.
The full flow, step by step
- The client sends an unauthenticated request to your MCP server and gets a 401 with a
WWW-Authenticateheader pointing at the protected-resource metadata — this is how the client discovers your AS. - The client registers itself with the AS via DCR and receives a client_id.
- The client generates a PKCE verifier + challenge and opens the browser to the AS's authorize endpoint, naming your server as the
resource. - The user signs in and approves; the AS redirects back with a one-time code.
- The client exchanges code + PKCE verifier for an access token that is audience-bound to your server.
- Every subsequent MCP request carries the token; your server verifies signature, expiry and audience on each call.
What breaks when you skip a piece
- No protected-resource metadata / wrong 401 — clients can't discover your AS. The symptom: the server "works" with curl but silently fails to install from Claude Desktop. This is the most common failure in the wild.
- No DCR — the connect flow dead-ends at "unknown client" for anyone but you.
- No PKCE (or PKCE accepted but not enforced) — the flow appears to work but intercepted authorization codes become exchangeable. Invisible until exploited.
- No audience check (RFC 8707) — tokens for other servers are accepted by yours, and yours are accepted elsewhere. Also invisible until exploited.
Common implementation mistakes
- Substring-matching redirect URIs instead of exact-match comparison.
- Allowing authorization codes to be exchanged more than once.
- Accepting
plainPKCE instead of requiringS256. - Verifying token signature and expiry but forgetting the audience claim — RFC 8707 only protects you if the RS actually checks it.
- Long-lived access tokens as a substitute for refresh tokens — shorter access-token lifetime plus refresh is the OAuth 2.1 posture.
Build it or get it built-in
None of this is exotic, but it is a genuine authorization-server implementation — days of work plus ongoing spec-tracking, and mistakes are security bugs rather than build failures. If you want to own it, the hosting guide walks through the deployment options. If you'd rather not: every server generated on BuildMyMCPServer ships behind an OAuth 2.1 AS with PKCE, DCR and Resource Indicators already wired — the flow above is what our OAuth docs implement, and the free tier includes it.
Skip the boilerplate — describe your tool, get a hosted MCP server.
BuildMyMCPServer generates the TypeScript server, wraps it in OAuth 2.1 and deploys it to a public Streamable HTTP URL for Claude, Cursor and ChatGPT. Free tier, source export, no lock-in.
Start building →