OAuth 2.1 Authentication for MCP Servers
The specification's own authentication method: an authorization-code flow the MCP client discovers and runs for itself, given nothing but the server's URL.
- Method
- OAuth 2.1
- Platform
- Any platform
- MCP Specification
- 2026-07-28
How it works
The client sends an unauthenticated request and gets 401 with a WWW-Authenticate header naming the server's RFC 9728 protected resource metadata. That document names the authorization server; the client then discovers the authorization server's own metadata, obtains a client_id, and runs an authorization-code flow with PKCE and an RFC 8707 resource parameter on both the authorization and the token request. The server validates that the token's audience is itself. Every step but the last happens inside the MCP client, which is why this works in Claude, ChatGPT, Cursor and VS Code and does not work in an agent framework whose MCP configuration accepts only a URL and a headers map.
- Best for
- Servers that must be reachable by MCP clients you do not control, or listed in a connector directory
- MCP spec
- The spec's own flow — though the spec still makes authorization OPTIONAL
- Works with
- Claude, ChatGPT, Cursor and VS Code run it themselves; among agent SDKs, only those shipping an OAuth provider
- Effort
- Days — most of it is the authorization server, not MCP
The exchange
Each step in the exchange, in order.
- MCP client in your agent or host
- Browser the user signs in
- Authorization server issues the token
- MCP server resource server
-
MCP client to MCP server
POST /mcp no Authorization header -
MCP server to MCP client
401 Unauthorized WWW-Authenticate: Bearer resource_metadata=… -
Two documents, two hops. RFC 9728 protected resource metadata names the authorization server; RFC 8414 metadata, or OpenID Connect Discovery, describes it. If code_challenge_methods_supported is absent from that second document, a conforming client MUST refuse to proceed.
MCP client to MCP server
GET …/oauth-protected-resource/mcp -
MCP server to MCP client
200 OK authorization_servers, scopes_supported -
MCP client to Authorization server
GET …/oauth-authorization-server then /.well-known/openid-configuration -
Authorization server to MCP client
200 OK code_challenge_methods_supported: [S256] -
The browser step. There is no headless variant of it: a framework whose MCP configuration takes only a URL and headers stops here, and the request never reaches /authorize at all.
Inside MCP client
Get a client_id, build the PKCE pair, open a browser pre-registered, then CIMD, then DCR -
Browser to Authorization server
GET /authorize code_challenge + resource=https://mcp.example.com/mcp -
Authorization server to Browser
302 to redirect_uri code + iss -
Browser to MCP client
Authorization code callback code, state, iss -
The resource parameter goes on both the authorization request and the token request, and the server checks the audience it produced. That check is what stops this token working at a different MCP route.
MCP client to Authorization server
POST /token code_verifier + resource -
Authorization server to MCP client
200 OK access_token, aud = the MCP server -
MCP client to MCP server
POST /mcp Authorization: Bearer <access-token> -
Inside MCP server
Check the token was issued for this server -
MCP server to MCP client
200 OK tool result
Connect your agent
Pick your language and SDK.
When to use something else
Use this when
- The server has to be reachable by MCP clients you do not control, or listed in a connector directory: Anthropic's submission requirements say to use OAuth 2.0 for authenticated services.
- Calls are made on behalf of a named person, and the audit trail has to identify that person rather than a key.
- You already run an identity provider and want its sign-in, its consent screen and its revocation to govern MCP access too.
Use something else when
- The caller is a cron job or a CI step. The authorization-code grant needs a browser and a person, and there is no headless variant of it.
- Your framework's MCP configuration takes only a URL and a headers map. The Claude Agent SDK documents this plainly: it does not open a browser or run an interactive OAuth flow, and a challenged server is reported
needs-authwhile the run continues without its tools. - Both ends belong to you and no end user's identity needs to reach the API behind the server, where a static key is less machinery for the same result.
This is the spec's own flow, and the spec still makes it optional: "Authorization is OPTIONAL for MCP implementations", with HTTP transports SHOULD — not MUST — conforming. 2026-07-28 is the current protocol version. Two of its changes alter the flow above: Dynamic Client Registration is marked Deprecated in favour of Client ID Metadata Documents, and clients MUST validate a present RFC 9207 iss parameter against the recorded issuer before redeeming the authorization code.
What lands on you next
A token proves who the caller is. It says nothing about what they may call, what they did call, or how often — and those are the next three tickets.
-
The token validates. Which of our tools can that caller reach?
-
mcp-capability-filter-inboundA static per-route allow-list of tools, prompts and resources. A call to a hidden tool is refused with
MethodNotFoundbefore it is forwarded. There is no per-user axis here — that is the row below. -
Only the finance group should reach this route. Where does that live?
-
require-user-claims-inboundA rule on the validated token's claims — group, role, tenant, subject — evaluated on the route and answering
403. Per-object decisions go to an AuthZEN or FGA policy on the same route; OpenFGA is Enterprise. -
A token was rejected at 2am. Which one, and why?
-
mcp_auth_downstream_token_validatedAuth-lifecycle events carry named reason codes,
missing_tokenandinvalid_audienceamong them. Bearer tokens, authorization codes and PKCE verifiers are never logged. -
One agent looped and made forty thousand tool calls. Can we cap that per user?
-
rate-limit-inboundrateLimitBy: "user"counts against the authenticated subject rather than a shared egress IP, so the budget follows the identity the token established.
All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out.
Common questions
Does MCP require OAuth?
No. The specification states that authorization is OPTIONAL for MCP implementations, and that HTTP-based transports SHOULD — not MUST — conform to it. What OAuth buys you is reach: a hosted client completes the whole flow given nothing but your URL, and Anthropic's connector-directory submission requirements say to use OAuth 2.0 for authenticated services.
Which spec revision is current, and what changed for OAuth?
2026-07-28 is the current protocol version. For authorization, it deprecates Dynamic Client Registration in favour of Client ID Metadata Documents, requires clients to validate a present RFC 9207 iss parameter, requires an application_type on dynamic registration, and requires client credentials to be keyed by the issuer that granted them.
Client ID Metadata Documents or Dynamic Client Registration?
A client supporting everything should try a pre-registered client id first, then a Client ID Metadata Document if the authorization server advertises client_id_metadata_document_supported, then RFC 7591 registration if it advertises a registration_endpoint, and only then prompt the user. CIMD is a SHOULD in both revisions; registration is a MAY and is Deprecated as of 2026-07-28. A CIMD client id is also portable: 2026-07-28 requires registered credentials to be keyed by the issuer that granted them and re-registered when the authorization server changes, whereas a CIMD client id is an HTTPS URL and there is nothing to re-register.
Why does my agent framework never open a browser for OAuth?
Because it has no browser and no callback URL. The Claude Agent SDK says so directly: the SDK does not open a browser or run an interactive OAuth flow, and a server that answers with an authorization challenge is reported needs-auth while the run continues without its tools. Complete the flow in your own application and pass the resulting access token in the server's headers.
Do I still send the resource parameter if my provider ignores it?
Yes. MCP clients MUST send this parameter regardless of whether authorization servers support it, on both the authorization request and the token request, using the MCP server's canonical URI. The Microsoft identity platform is the common case that ignores it — its v2.0 protocol uses scopes instead of resource, so you ask for the audience with a scope — and the parameter still goes out.
Can my MCP server forward the client's token to the API behind it?
No, and this is the one prohibition in the whole flow. The security best practices document is explicit: MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server, and the authorization spec adds that a server acting as a client to an upstream API MUST NOT pass through the token it received. Mint or exchange a separate upstream credential.
What if the 401 has no WWW-Authenticate header?
The client keeps going. A server MUST implement one of two discovery mechanisms, and clients MUST support both: use the resource_metadata URL from the header when present, otherwise construct and request the well-known URIs — the path-scoped form /.well-known/oauth-protected-resource/mcp first, then the root. A missing header is therefore not fatal on its own; a missing metadata document is.
Is PKCE optional if the authorization server does not advertise it?
The opposite: absence is a stop condition. Clients MUST implement PKCE and MUST verify support before proceeding, and because neither OAuth 2.1 nor PKCE defines a discovery mechanism, that verification is the presence of code_challenge_methods_supported in the authorization server's metadata. If it is absent, clients MUST refuse to proceed. S256 is required wherever the client is technically capable of it.
One policy engine for APIs, AI, and MCP
Put your MCP servers behind a gateway that speaks every identity provider, filters tools per role, and logs every call.