Est.
MCP 101Long read

MCP OAuth2 Authorization Code Flow Walkthrough

Learn how to implement OAuth 2.1 authorization for HTTP-based MCP servers serving multiple users.

Reporter · · 10 min read
Cover illustration for “MCP OAuth2 Authorization Code Flow Walkthrough”
MCP 101 · August 4, 2026 · 10 min read · 2,230 words

The first time you set up a local MCP server, authentication feels like someone else's problem. You drop credentials into an environment variable, point your client at a stdio transport, and the whole thing just works. No OAuth, no redirects, no token lifecycle to manage. It's clean. Almost suspiciously clean.

Then someone asks you to expose that server over HTTP so multiple users can hit it from different clients, and the clean feeling evaporates immediately.

That's the architectural moment this walkthrough is about. The shift from a trusted local process to a cloud-hosted resource serving many clients on behalf of many users is precisely the context OAuth 2.1 was designed for. API keys collapse that distinction entirely; they can't tell you which user is making a request. Mutual TLS is theoretically sound but practically brutal to distribute across desktop apps, agents, and automation scripts that don't control their own certificate stores. OAuth 2.1 threads the needle: it delegates user-scoped access without handing credentials to the client, and it scales across every client type in the MCP ecosystem.

One scoping note before we go further. This flow applies exclusively to HTTP-based transport, specifically Streamable HTTP. If you're using stdio transport, you should not follow this spec; credentials come from the environment, and that's intentional. The SSE transport, worth mentioning, was deprecated in the June 2025 spec revision and is not a valid target for new implementations. And while authorization is formally optional in the MCP spec, when you're running HTTP-based transport, conformance is the practical expectation.

How the spec has evolved and which version governs this walkthrough

Four versions of the MCP spec exist: the initial November 2024 release, then March 2025, June 2025, and the current November 2025 revision. The original release had no authorization coverage at all. That's not a criticism; the early assumption was local, trusted environments.

The June 2025 revision was the first serious enterprise-facing upgrade. Its most consequential change was architectural clarity: it explicitly separated the MCP server's role as a resource server from the authorization server's role as token issuer. It also removed the fallback default endpoints at /authorize, /token, and /register in favor of mandatory RFC 9728 Protected Resource Metadata discovery. If your implementation relied on those default paths, it is now non-conformant.

The November 2025 revision, which governs this walkthrough, added four things worth internalizing before you write a single line of code. First, it explicitly banned the plain PKCE method; S256 is now the only permitted option. Second, it required the resource parameter from RFC 8707 in all token requests, which is the defense against token mis-redemption across servers. Third, it mandated that 401 responses include a properly formatted WWW-Authenticate header pointing to the authorization server. Fourth, it added explicit security guidance on CSRF and redirect URI validation, and introduced step-up authorization via 403 responses.

Earlier implementations that relied on default endpoint fallbacks, permitted plain PKCE, or omitted the resource parameter are not compliant with the current spec. This isn't bureaucratic version-churn; each of those changes closes a concrete attack surface.

Table: MCP Spec Evolution: Key Authorization Changes. Compares Authorization Coverage, Server Role Separation, Discovery Endpoints, PKCE, and 2 more by Nov 2024, Jun 2025 and Nov 2025.

The three roles in the flow: MCP client, MCP server, and authorization server

Diagram: Nine-Step OAuth 2.1 Flow for MCP Over HTTP. Visualizes: Visualize the end-to-end OAuth 2.1 authorization flow for MCP HTTP transport as a numbered sequence across three named parties: MCP Client, MCP Server, and Authorization Server.

The OAuth vocabulary maps cleanly onto MCP's architecture, and getting the mapping right before you build saves a lot of confused debugging.

The MCP client is the OAuth client: the agent, desktop application, or automation script requesting access on behalf of a user. The MCP server is the OAuth resource server: it holds the protected tools and data, but it does not issue tokens. The authorization server is the OAuth identity provider; it issues tokens and is often a completely separate service.

That last separation is the architectural clarification the June 2025 spec made explicit, and it has a practical consequence most people miss on first read. MCP server operators don't have to run their own auth infrastructure. They can point to an existing identity provider they already operate or trust. The MCP server just validates tokens; it doesn't need to know how they were issued.

The flow's structure follows directly from these three roles. The client talks to all three in sequence. The steps below trace exactly that conversation.

Step 1 — The cold start: the 401 response that initiates discovery

The client makes an unauthenticated request to the MCP server. This is intentional. You don't pre-configure the client with knowledge of the authorization server; instead, the server tells you where to go.

The server responds with a 401 Unauthorized. Per the November 2025 spec, this is not an ambiguous rejection; it is a deterministic handoff carrying a mandatory WWW-Authenticate header. That header includes a resource_metadata parameter that points to the server's Protected Resource Metadata URL.

The design here is worth appreciating. The 401 is load-bearing. It's not just "you're not authenticated"; it's "here is exactly where to start the process." A 401 without a correctly formed WWW-Authenticate header doesn't just fail gracefully; it breaks the entire discovery chain downstream. Every step that follows depends on this first signal being well-formed.

Step 2 — Protected Resource Metadata discovery (RFC 9728)

The client fetches a GET request to /.well-known/oauth-protected-resource on the MCP server. This path is not configurable. RFC 9728 specifies it exactly, and any deviation means the discovery fails. Alternatively, the client may use the resource_metadata URL from the WWW-Authenticate header directly if one was provided.

The response is a JSON document. Two fields are required: resource, which is the canonical URL of the MCP server, and authorizationservers, which is an array of authorization server URLs the client should use. Optional but practically useful fields include scopessupported, bearermethodssupported (always ["header"] for MCP), and resource_documentation.

After this fetch, the client has one critical piece of information it lacked before: which authorization server to talk to. It doesn't yet know that server's concrete endpoints. That's the next step.

Step 3 — Authorization Server Metadata discovery (RFC 8414)

The client fetches GET {authorizationserver}/.well-known/oauth-authorization-server. The response document exposes the concrete endpoints and capabilities the client needs to proceed: authorizationendpoint, tokenendpoint, and registrationendpoint if the AS supports Dynamic Client Registration. It also signals whether Client ID Metadata Documents are supported via clientidmetadatadocumentsupported, and lists supported scopes and other AS capabilities.

OpenID Connect Discovery 1.0 is also supported as an alternative path for locating AS metadata, which adds flexibility without altering any of the downstream steps.

After this fetch, the client has a complete map of the authorization landscape. The immediate next decision is how to identify itself to the authorization server.

Step 4 — Client registration: three paths in priority order

The spec defines a priority order that clients should follow, and the ordering reflects both practical realities and the forward direction of the specification.

First, use pre-registered client information if it's already available for this server. Second, use Client ID Metadata Documents if the AS signals support. Third, use Dynamic Client Registration per RFC 7591 if the AS exposes a registration_endpoint. Fourth, if none of those apply, prompt the user to supply client information manually.

CIMD is the forward-looking default path. The client uses an HTTPS URL as its client identifier, and that URL resolves to a JSON document containing client metadata. It removes the need for runtime DCR handling in most new implementations.

Here's a data point from the brief that's worth sitting with: in testing across 660 authorization server endpoints, only 27, roughly 4%, actually supported Dynamic Client Registration. DCR remains in the spec for backwards compatibility, and the November 2025 revision shifted it to MAY rather than MUST for both clients and authorization servers. The practical implication is direct: most SaaS-hosted MCP servers proxy to their own APIs and use authorization servers that don't support DCR. Pre-registration or CIMD is the realistic path for most implementations you'll actually encounter.

Step 5 — PKCE credential generation before the redirect

Before the client initiates any redirect, it generates a codeverifier: a cryptographically random string that never travels over the wire. It then hashes that verifier using SHA-256 to produce the codechallenge, which does get sent in the authorization request.

S256 is the only permitted method as of the November 2025 spec. Plain is explicitly banned, and the prohibition is not arbitrary: plain PKCE provides no meaningful security benefit over omitting PKCE entirely, since the verifier and challenge are identical.

The security property PKCE provides is particularly relevant in MCP contexts because many clients, agents, containers, serverless functions, operate in environments where storing a client secret securely is difficult or simply infeasible. PKCE means that intercepting the authorization code in transit is not sufficient to obtain a token. An attacker would also need the original verifier, which they never saw. The verifier lives in the client's session state and doesn't travel until the code exchange.

Step 6 — The authorization redirect: constructing the request to the AS

The client constructs a redirect to the authorization server's authorizationendpoint with a specific set of parameters. clientid is the identifier obtained during registration. redirecturi is the client's OAuth callback endpoint and must be pre-registered; exact match validation is required. scope carries the requested permissions. responsetype=code tells the AS you want an authorization code flow. codechallenge and codechallenge_method=S256 carry the PKCE material. state is an opaque random value for CSRF protection. And resource, per RFC 8707, is the canonical URI of the MCP server.

That resource parameter deserves emphasis. The spec defines a scope selection priority: use the scope from the initial 401's WWW-Authenticate header if one was provided, then fall back to all scopes in scopes_supported from the Protected Resource Metadata. The least-privilege principle applies directly here; clients should request only the scopes necessary for what they're actually doing.

The resource parameter is what binds the resulting token to this specific MCP server. Its absence is the root cause of token mis-redemption attacks, where a token obtained for one server gets presented to another. Redirect URI security is equally non-negotiable: authorization servers must validate exact redirect URIs against pre-registered values. A lenient redirect URI validator is a direct path to account takeover.

The user lands at the authorization server's login and consent interface. They authenticate if they aren't already, and they see a consent screen describing what access the agent is requesting on their behalf. On approval, the AS issues an authorization code and redirects back to the client's registered redirect_uri.

That redirect carries two things: the authorization code and the state value the client sent in step 6. The client must verify that the returned state matches what it originally generated. This is the CSRF defense, and skipping it is the kind of omission that looks harmless until it isn't.

The authorization code itself is short-lived and single-use. It is not a token. It cannot be presented directly to the MCP server. It's a one-time credential whose only purpose is to be exchanged for tokens in the next step.

Step 8 — Code exchange: trading the authorization code for tokens

The client makes a POST to the AS's tokenendpoint. The request body carries the grant type set to authorizationcode, the code received in step 7, the redirecturi matching the one used in the authorization request, the codeverifier from step 5, and the resource parameter identifying the MCP server.

The AS hashes the codeverifier using S256 and compares the result to the codechallenge it stored from the authorization request. If they match, the token is issued. If they don't, the request fails. This is PKCE completing its function.

The resource parameter in this request is what instructs the AS to bind the token's audience to this specific MCP server. Including it in the authorization request but omitting it here is insufficient; both requests need it to properly constrain the token. The response includes an access token, typically a JWT, token type, expiration, and optionally a refresh token.

Step 9 — Using the access token with the MCP server and what the server validates

The client attaches the access token to subsequent MCP requests via the Authorization header as a Bearer token. The Protected Resource Metadata document specifies bearermethodssupported as ["header"] for MCP, so this is the only accepted delivery method. Query parameters and request body delivery are not in scope.

On every request, the MCP server has to do real validation work. It verifies the token signature. It checks expiration. It confirms the token was issued by a trusted authorization server. It verifies that the scopes cover the specific operation being requested. And it validates the token audience, confirming the token was issued for this specific resource server per RFC 8707. Tokens not bound to this server must be rejected.

That audience validation is where the whole structure closes. Every preceding step, the resource parameter in the authorization request, the resource parameter in the token request, the AS binding the token's audience, it all converges here. The MCP server's audience check is what makes the system resistant to token reuse across servers, which is the attack the November 2025 spec revision was specifically designed to preclude.

The flow isn't complicated once you see it as a single conversation across three parties. The 401 starts it. Discovery maps the landscape. Registration establishes identity. PKCE and state protect the redirect. The code exchange proves the flow wasn't intercepted. And the server's validation on every subsequent request enforces the boundary the entire flow was constructed to create.

Sources

  1. modelcontextprotocol.io
  2. obsidiansecurity.com
  3. descope.com
  4. aembit.io
  5. stackoverflow.blog
  6. developers.cloudflare.com
Filed underMCP 101

More in MCP 101