MCP OAuth and Authentication Flows
MCP servers shifted from issuing tokens to validating them, narrowing security responsibility.

Four specification versions shipped in roughly twelve months: November 2024, March 2025, June 2025, and November 2025. That cadence tells you something. The team was correcting course as production deployments surfaced problems that local development never exposed, not polishing.
The original MCP architecture was elegantly simple. Server and client ran together, locally, often in the same process or at least on the same machine. No network boundary, no untrusted caller, no meaningful authentication surface to defend. The predominant approach was static API keys in config files or environment variables, entirely reasonable for that context. But those keys were shared, unscoped, and unrevocable. If one leaked, you had no blast radius control whatsoever. Full access, immediately, with no recovery path except rotating the key and hoping nothing else broke.
The November 2024 spec contained no authorization section. That absence was a reflection of the local-first architecture it was built around, not an oversight.
March 2025 was where OAuth 2.1 entered the picture formally, with MCP servers positioned as their own OAuth authorization servers. Conceptually coherent, but the operational burden was significant: every team shipping an MCP server now owned token issuance, client registration, key management, and the full catalog of OAuth edge cases.
June 2025 was the more consequential pivot. MCP servers were reclassified as OAuth Resource Servers, meaning they accept and validate tokens but do not issue them. Token issuance moved to external identity providers, the Entra IDs and Oktas that enterprises already operate. That separation changes the security surface your team is responsible for. The June spec also made Resource Indicators from RFC 8707 required, explicitly prohibited token passthrough, a practice that had been common in the field, and deprecated Server-Sent Events transport in favor of Streamable HTTP. A dedicated security best practices page was added, the kind of addition that signals the working group had seen enough production incidents to warrant explicit guidance.
November 2025 is the current production baseline. PKCE became mandatory for all client-side applications. Client ID Metadata Documents were introduced and elevated to the preferred registration path. Step-Up Authorization Flow was formally defined, allowing clients to request additional scopes incrementally at runtime. Machine-to-machine OAuth client credentials support arrived alongside enterprise IdP policy controls.
The implication teams consistently underestimate: implementations built against March 2025 carry architectural assumptions the June and November specs explicitly reversed. If your MCP server is still acting as its own authorization server, you're operating an architecture the specification has moved away from, and the security properties you think you have do not hold.
The three OAuth roles MCP distributes across client, server, and authorization server
OAuth 2.1 defines three core roles, and understanding how MCP distributes them is the prerequisite for everything else here.
The MCP client is the OAuth client. It makes protected resource requests on behalf of a resource owner, typically a human user or an automated system with defined credentials. The MCP server is the OAuth resource server. It accepts requests and responds using access tokens, but it does not issue them. The authorization server is a separate entity entirely: an external identity provider handling user interaction, consent, and token issuance.
Before June 2025, MCP servers played both resource server and authorization server roles simultaneously. One team, one codebase, full ownership of a security surface that most application developers have never had to reason through before. After the change, token issuance is delegated to infrastructure purpose-built for it. The MCP server's security responsibility narrows to validating tokens it receives, and that validation plugs into enterprise identity infrastructure that already has audit trails, revocation mechanisms, and policy controls.
Authorization is technically optional per the specification for MCP implementations broadly. But for HTTP-based transports, the spec says implementations SHOULD conform to the authorization spec, and in practice "should" collapses to "must" the moment you're serving any caller you don't fully control. STDIO-based transport is different; there you retrieve credentials from the environment and skip the OAuth flow entirely. The transport boundary is also an authorization boundary, which is easy to miss.
Why OAuth 2.1 specifically, rather than the more widely deployed OAuth 2.0? The delta between them is not academic. PKCE is required for all authorization code flows in 2.1. The implicit grant is dropped entirely. Exact redirect URI matching is required. Each of those constraints directly addresses the attack surface created by MCP clients running as local desktop applications, containers, or serverless functions, environments where the implicit grant's security properties were always inadequate and where redirect URI slop creates real interception risk.
Walking through the five-stage authorization flow a client executes against a protected MCP server
Stages one and two happen once per client-server pair. Stages three through five repeat each time a fresh token is needed. The early stages involve network round-trips and user interaction you don't want to trigger unnecessarily; that distinction matters when you're debugging latency and wondering where the time went.
Stage one is the initial connection. The client connects to a protected MCP server. The server rejects with a 401 Unauthorized response carrying a mandatory WWW-Authenticate header, and inside that header is a resource_metadata parameter pointing to the Protected Resource Metadata document. The 401 is not a failure; it's an instruction.
Stage two is metadata discovery. The client fetches the PRM document from that URL. The document advertises where the authorization server lives. This eliminates ambiguity about which authorization server is legitimate for this resource. A client that discovers the AS through the PRM document cannot be redirected to a rogue AS by a manipulated response. The discovery path is anchored to the resource itself.
Stage three is client registration, and the November 2025 spec defines a specific priority order. First, the client checks whether it has pre-registered credentials for this authorization server; VS Code, for example, has a hardcoded client_id for Entra ID already in its configuration. If that exists, use it. If not, check whether the AS advertises support for Client ID Metadata Documents. If it does, use CIMD. If CIMD isn't supported, check for a Dynamic Client Registration endpoint. If none of those paths are available, manual entry is the last resort.
Stage four is the PKCE-protected authorization code flow. The client generates a codeverifier, hashes it to produce a codechallenge, redirects the user's browser to the AS's login page, the user authenticates and consents, the AS returns a short-lived authorization code, and the client redeems that code at the token endpoint along with the original verifier.
Stage five is bearer token usage. Every MCP request carries the access token in an Authorization: Bearer header. The MCP server validates the token: issuer, audience, expiry, and OAuth scopes. If the scopes are insufficient for the requested operation, the server returns 403 and the client initiates Step-Up Authorization to request the additional scope incrementally rather than failing hard.
How PKCE closes the code interception threat that matters most in MCP environments
The threat PKCE is designed to neutralize is specific: an authorization code intercepted in transit can be replayed to obtain a token. Without PKCE, possessing the code is sufficient. That's a meaningful vulnerability in any environment, but it's particularly acute for MCP clients because they frequently run as local desktop applications, containerized services, or serverless functions, environments where storing a long-lived client secret securely is either difficult or entirely infeasible.
The mechanism is straightforward. Before initiating the authorization flow, the client generates a cryptographically random string called the codeverifier. This string never leaves the client during the initial request. The client computes a SHA-256 hash of that verifier, producing the codechallenge, which is what gets sent to the authorization server at the start of the flow. The AS stores that challenge.
When the client later presents the authorization code at the token endpoint, it sends the original code_verifier. The AS hashes it, compares the result against the stored challenge, and issues a token only if they match. An attacker who intercepts the authorization code cannot redeem it without the verifier, which they never saw. The code alone is inert.
The November 2025 MCP specification is unambiguous: S256 is the only permitted challenge method. The plain method, which provides no meaningful security improvement over an absent PKCE implementation, is excluded. If a server's metadata advertises codechallengemethods_supported without including plain, that's intentional.
OAuth 2.1 making PKCE mandatory for all flows, not just public clients, reflects a hard lesson from OAuth 2.0 deployments. The original optional-PKCE model produced implementations with wildly inconsistent protection. Some used it, some didn't, and the difference was often invisible until something went wrong. I've seen that exact failure mode, a confidential client with no PKCE, exploited in a staging environment that someone had misconfigured to accept real credentials. Instructive, if not catastrophic.
Why Dynamic Client Registration gave way to Client ID Metadata Documents as the preferred registration path
Dynamic Client Registration's original promise was genuinely appealing: just-in-time registration for clients the authorization server has never seen before, no manual setup required. A client POSTs its metadata, name, redirect URIs, requested grant types and scopes, to a registration endpoint. The AS validates, issues a clientid and optionally a clientsecret, and the client proceeds with a standard authorization code flow.
The operational reality was less tidy. Most authorization servers deployed in production don't support DCR. The compatibility gap was wide enough that teams regularly hit walls with their target IdP and had to improvise. More troubling, an open registration endpoint is functionally an unauthenticated write API sitting directly on your security infrastructure. Managing the resulting proliferation of client registrations became an operational burden teams hadn't anticipated.
Client ID Metadata Documents reframe the problem at a conceptual level. Instead of treating clientid as an opaque identifier assigned by the authorization server, CIMD makes clientid a URL. The URL hosts the client's own metadata document. When the authorization server needs to know something about the client, it fetches and validates that document at runtime. No sprawling registration database. No unauthenticated POST endpoint. The metadata lives where the client can maintain it.
That document contains everything the AS needs: redirect URIs, PKCE settings, JWKS locations for key-based authentication, application type. For public clients without secrets, you set tokenendpointauthmethod to none and rely on PKCE for security. For confidential clients, you specify privatekeyjwt and provide a jwksuri pointing to your public keys.
DCR didn't disappear from the specification. It's still there, still supported by major clients, still the appropriate path in environments where the authorization server has native DCR support. But the priority order in November 2025 is explicit: CIMD first, DCR as a fallback. If you're building a new integration today, check whether your target authorization server supports CIMD before assuming DCR is the path.
How scope strategy and Step-Up Authorization prevent over-permissioned tokens at runtime
Least-privilege scoping is one of those principles everyone agrees with in the abstract and many implementations violate in practice. The violation usually isn't malicious; it's defensive. Developers request a broad set of scopes upfront because they're uncertain which operations the client will need, and they'd rather not handle scope-related failures at runtime. The result is over-permissioned tokens that persist in token storage far longer than the operations that warranted them.
The November 2025 spec provides a structured path for discovering which scopes to request initially. If the 401 response's WWW-Authenticate header includes a scope parameter, use those scopes. If it doesn't, consult the scopes_supported field in the PRM document. The discovery mechanism exists so clients don't have to guess or default to requesting everything.
Step-Up Authorization is the runtime complement to careful initial scoping. When a client attempts an operation that requires a scope absent from the current access token, the server returns 403 rather than a generic error. The client interprets that 403 as a signal to request the additional scope incrementally, initiating a new authorization request scoped specifically to what the operation requires. The broader session persists; only the missing scope is negotiated.
The security implication is real. A well-scoped initial token combined with Step-Up Authorization is safer than a maximal-scope token because the blast radius of a stolen or exfiltrated token is bounded by what was granted at issuance. A token that grants read access to one calendar is a different class of risk than one granting administrative access to everything the user is entitled to touch. I've had that conversation with engineering leads who thought broad scoping was "just simpler," and the simplicity argument evaporates quickly when you're explaining to someone why a leaked token touched seventeen systems instead of two.
Resource Indicators from RFC 8707 reinforce this at the resource level. Clients must include the resource parameter when requesting tokens, binding each token to its intended MCP server. A token issued for one resource cannot be legitimately reused against a different resource, even if the scopes nominally overlap.
Three classes of vulnerability that emerge when the authorization flow is implemented incorrectly
These vulnerabilities are not novel attack research. They follow directly from specification requirements that were ignored, misunderstood, or implemented against an earlier version of the spec that no longer reflects current guidance. They aren't exotic attack chains requiring sophisticated adversaries. They're implementation errors that compound over time.
Token passthrough has been explicitly prohibited since June 2025. The pattern looks innocent: an MCP server receives a token from the client and forwards it to an upstream API call. The problem is a confused deputy attack. The upstream service sees a token and, depending on how it validates audience claims, trusts it even though the token was never intended for that service. Audience validation failures are common; many implementations check issuer and expiry carefully but are permissive about the aud claim. When an MCP server passes a client's token downstream, it launders an authorization that was never granted for that downstream operation. Each MCP server must obtain its own appropriately scoped tokens for any upstream calls it makes.
Redirect URI validation failures are the mechanism through which authorization codes get stolen. OAuth 2.1 requires exact redirect URI matching, which is not how every implementation works. Implementations built under earlier OAuth 2.0 guidance, or against libraries with permissive defaults, accept prefix matches, parameter variations, or subdomain wildcards. An attacker who can register a URI that passes a loose match but routes to infrastructure they control can intercept authorization codes in transit. Combined with absent or weak PKCE, that's a complete token theft chain. The November 2025 spec's insistence on S256 and exact URI matching exists because those partial-match patterns were real, demonstrated risks.
Metadata endpoint trust failures are easy to miss because the discovery phase feels procedural rather than security-critical. The Protected Resource Metadata document tells the client where to find the legitimate authorization server. If a client fails to validate that it's fetching the PRM document over TLS, doesn't verify the document's integrity, or doesn't anchor the authorization server location to the resource's own domain, an attacker with network position can substitute a different authorization server, one they control. The client then performs a perfectly valid OAuth flow against a rogue AS, obtains tokens the legitimate MCP server won't accept, and the user's credentials have been exposed to an unauthorized party. The discovery mechanism is a security-critical path, not administrative scaffolding.
All three classes share the same root cause: security properties that the specification constructs carefully across multiple stages get eroded when individual stages are implemented in isolation, without understanding why each requirement exists. You can implement stages one, three, and five correctly and still be completely exposed if two or four are wrong. The flow is interdependent; partial compliance is not a security posture.


