MCP Authorization Explained: OAuth 2.1, RFC 9728 and CIMD in 2026

Oct 20, 2026 · 12 min · Ajay Kumar

The first remote MCP server I put behind a login took me longer to authorize than to build, and most of that time went on reading a spec that kept changing under me. MCP authorization has had four revisions in sixteen months, and the version most blog posts describe is already out of date: Dynamic Client Registration, which nearly every tutorial still opens with, was formally deprecated in the 2026-07-28 specification. This post explains how MCP authorization works today, piece by piece: the OAuth 2.1 profile, protected resource metadata, resource indicators, Client ID Metadata Documents, the token passthrough ban, and how enterprises plug their own identity provider in. If you want the hands-on version, the companion guide on deploying a secure remote MCP server with OAuth builds one end to end.

What is MCP authorization, and when does it apply?

MCP authorization is a profile of OAuth 2.1 for the HTTP transport. The specification is explicit that it is optional, that HTTP implementations should conform to it, and that stdio servers should not use it at all and should read credentials from the environment instead.

There are three roles, and they map straight onto standard OAuth:

  • The MCP server is an OAuth 2.1 resource server. It accepts access tokens and checks them. It does not log anyone in.
  • The MCP client (Claude, VS Code, Cursor, your own agent) is an OAuth 2.1 client acting for a user.
  • The authorization server is whatever issues tokens: Keycloak, Okta, Entra ID, Auth0, Cognito, or your own.

That separation is the single most important fact about MCP auth, and it was not always true. The 2025-03-26 revision introduced an OAuth 2.1 framework in which discovery fetched authorization server metadata from the MCP server's own base URL, which in practice made every MCP server its own authorization server. Aaron Parecki's April 2025 post, Let's fix OAuth in MCP, argued for splitting the roles and pointing clients at a separate authorization server through RFC 9728. The 2025-06-18 revision adopted that: MCP servers became resource servers, and clients were required to send RFC 8707 resource indicators.

How does an MCP client discover where to log in?

Discovery is a chain of three documents, and every link is a published standard.

  1. The client calls the MCP endpoint with no token. The server answers 401 Unauthorized with a WWW-Authenticate header carrying a resource_metadata URL, and optionally a scope hint.
  2. The client fetches that URL, a RFC 9728 Protected Resource Metadata document (published April 2025, authored by Mike Jones, Phil Hunt and Aaron Parecki). Its authorization_servers field names at least one issuer.
  3. The client fetches the issuer's metadata, trying RFC 8414 OAuth metadata first and OpenID Connect Discovery second, in a fixed order the spec spells out for issuers with and without a path component.

Servers must implement one of two ways to publish the metadata: the header, or a well-known URI either at the endpoint path (/.well-known/oauth-protected-resource/mcp) or at the root. Clients must try the header first and then both well-known locations, in that order. This is what it looks like from a Python SDK v2.3.0 server I ran locally with curl, against a Keycloak 26.8.1 realm:

# MCP Python SDK 2.3.0 server on :8000, Keycloak 26.8.1 realm "mcp" on :8081
$ curl -si -X POST http://127.0.0.1:8000/mcp -d '{}' | grep -i www-authenticate
www-authenticate: Bearer error="invalid_token", error_description="Authentication required",
  resource_metadata="http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp"

$ curl -s http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp
{"resource":"http://127.0.0.1:8000/mcp",
 "authorization_servers":["http://127.0.0.1:8081/realms/mcp"],
 "scopes_supported":["notes:read"],
 "bearer_methods_supported":["header"]}

The spec says scopes_supported should be the minimal set for basic functionality, and clients with no scope hint request everything listed there. Publish admin:* in that list and every client will ask for it on first login.

Why does MCP require resource indicators (RFC 8707)?

Because a token that works everywhere is a token any server can steal and reuse. The spec requires clients to send the resource parameter, set to the MCP server's canonical URI, in both the authorization request and the token request, "regardless of whether authorization servers support it". The server, in turn, must validate that each token was issued specifically for it as the audience, and must reject everything else.

The canonical URI rules are fussy for good reason: lowercase scheme and host, no fragment, and no trailing slash unless it means something. https://mcp.example.com/mcp and https://mcp.example.com/mcp/ are different strings, and an audience check is a string comparison.

The awkward part is identity provider support. Keycloak's own MCP integration page lists RFC 8707 as experimental behind a feature flag, and recommends a workaround: optional client scopes carrying an Audience mapper so the token's aud becomes the MCP server URL. Amazon Cognito's access token reference says aud is present only when the application requested a resource binding. Entra ID's claims reference says a v2.0 token's aud is always the API's client ID, not a URL. So in practice "validate the audience" means "validate whatever your IdP puts in aud for this server", and you should test it rather than assume.

Client ID Metadata Documents vs Dynamic Client Registration

This is where the spec moved most, and where the debate was loudest.

OAuth assumes clients are registered with the authorization server ahead of time. MCP breaks that: a user can paste any server URL into any client, and neither side has heard of the other. The first answer was RFC 7591 Dynamic Client Registration: the client POSTs its name and redirect URIs to an open /register endpoint and gets a client_id back.

The problems are structural, and Parecki's July 2026 post on the missing trust anchor in DCR lays them out. The endpoint must be unauthenticated, so anyone can register anything. The metadata is self-asserted, so an attacker can register "Claude" with Claude's logo and the consent screen looks identical. Registrations also pile up with no safe rule for deleting them.

Client ID Metadata Documents (CIMD) flip it around. The client_id is an HTTPS URL on a domain the client controls, pointing at a JSON document. The authorization server fetches it when it first sees the ID, checks that the client_id inside matches the URL exactly, and validates the redirect URI against the listed ones. No registration endpoint, no table of orphaned clients, and a domain the authorization server can put a policy on.

{
  "client_id": "https://app.example.com/oauth/client-metadata.json",
  "client_name": "Example MCP Client",
  "redirect_uris": ["http://127.0.0.1:3000/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

The 2025-11-25 revision added CIMD as the recommended mechanism (SEP-991) and dropped DCR to a MAY. The 2026-07-28 revision deprecated DCR outright, keeping it only for authorization servers that do not support CIMD. The client's preference order is now pre-registered credentials first, CIMD if the server advertises client_id_metadata_document_supported, DCR as a fallback, and asking the user last.

CIMD is not magic. The spec warns that it cannot stop localhost redirect impersonation: an attacker can present a legitimate client's metadata URL, bind any localhost port, and receive the code. Authorization servers must display the redirect hostname, should warn on localhost-only clients, and should treat the metadata fetch as an SSRF risk. Lookalike domains still work. What CIMD buys is a trust signal where DCR had none.

Spec revision What changed in authorization Source
2025-03-26 OAuth 2.1 framework introduced; MCP server effectively also the authorization server changelog
2025-06-18 MCP servers become OAuth resource servers; RFC 9728 discovery; RFC 8707 required of clients; security best practices page changelog
2025-11-25 OIDC Discovery accepted; incremental scope consent via WWW-Authenticate (SEP-835); CIMD recommended (SEP-991); well-known fallback when no header (SEP-985) changelog
2026-07-28 DCR deprecated in favour of CIMD; RFC 9207 iss validation (SEP-2468); credentials bound to their issuer (SEP-2352); application_type in DCR (SEP-837) changelog
MCP authorization, four spec revisions and one extension 2025-03-26 OAuth 2.1 framework server is also the AS 2025-06-18 server = resource server RFC 9728 + RFC 8707 2025-11-25 CIMD recommended DCR optional, step-up June 2026 extension Enterprise-Managed Auth ID-JAG via your IdP 2026-07-28 DCR deprecated RFC 9207 iss check Constant since 2025-06-18: server only validates tokens, audience-bound tokens, PKCE S256, no passthrough
Authorization milestones from the MCP specification changelogs and the MCP blog's Enterprise-Managed Authorization announcement of 18 June 2026.

What is the token passthrough ban?

Token passthrough is an MCP server taking the token a client sent it and forwarding it to a downstream API, or accepting a token that was minted for some other service. The spec forbids both: servers "MUST NOT accept or transit any other tokens", and if a server calls an upstream API it acts as a separate OAuth client and uses a separate token issued by the upstream authorization server.

The security best practices page gives the reasons, and they are all operational. Downstream rate limits and monitoring keyed on audience get bypassed. Logs on the downstream service show the wrong caller, so incident response loses the trail. A stolen token turns your MCP server into a free exfiltration proxy. And a server that starts as "just a proxy" has nowhere to add controls later.

The same page describes the confused deputy attack on MCP proxy servers: a proxy uses one static client ID with a third-party API, allows clients to register dynamically, and the third party sets a consent cookie. An attacker registers a client with their own redirect URI, sends the victim a link, the cookie skips consent, and the code lands at the attacker. The required fix is per-client consent owned by the MCP server, shown before the upstream redirect, with exact redirect URI matching and single-use state.

It is the old API gateway rule that every hop authenticates with its own credential, applied to a protocol where the caller is often a model. I covered how that plays out across agent identity more broadly in non-human identity in 2026.

Scopes, step-up and the other MUSTs

A few more requirements decide whether an implementation is safe:

  • PKCE with S256. Clients must use it and must refuse to proceed if the authorization server's metadata lacks code_challenge_methods_supported. Keycloak 26.8.1 advertises ["plain", "S256"], which I confirmed from a local realm's discovery document.
  • Step-up authorization. When a token lacks a scope, the server should answer 403 with error="insufficient_scope" and the scopes the operation needs, all in one challenge. The 2026-07-28 text makes clients union the new scopes with what they already had, so stepping up for files:write does not silently drop files:read.
  • Issuer validation. New in 2026-07-28, clients must record the expected issuer before redirecting and compare it to the iss parameter in the authorization response (RFC 9207), closing a mix-up attack where a code meant for one authorization server is redeemed at another.
  • Tokens in headers only. Authorization: Bearer on every request, never in the query string. The 2026-07-28 revision also removed protocol sessions and the Mcp-Session-Id header, which takes a whole session-hijacking class off the table.

Clients have failed too: CVE-2025-6514 had mcp-remote passing a server-supplied authorization_endpoint into a shell, and the spec now forbids non-HTTP(S) authorization URLs and shell-based URL opening. My MCP security overview has the longer list.

How does MCP work with an enterprise IdP like Okta or Entra?

Per-server OAuth consent does not scale to a company with forty MCP servers and an access policy. The answer is the Enterprise-Managed Authorization extension, announced as stable on 18 June 2026 and published in the ext-auth repository.

The flow has two exchanges. The client, already signed in to the corporate IdP, sends an RFC 8693 token exchange with its ID token and the target MCP server, and receives an Identity Assertion JWT Authorization Grant (ID-JAG) if IdP policy allows. It then presents the ID-JAG to the MCP server's authorization server as an RFC 7523 JWT bearer grant and gets an access token that, under this profile, must be audience-restricted to that MCP server. The user never sees a per-server consent screen; access follows groups and conditional access rules in the IdP.

The announcement names Okta, through Cross App Access, as the first supported IdP; Claude, Claude Code, Cowork and VS Code as clients; and Asana, Atlassian, Canva, Figma, Granola, Linear and Supabase as servers. On the SDK side, the MCP Python SDK v2.0.0 release added RFC 9207 issuer validation, the SEP-990 identity assertion flow and the client credentials extension, which is still a draft for machine-to-machine access.

This is the piece I would watch: it puts the access decision back where enterprise security teams already make it.

FAQ

Does an MCP server need to implement OAuth itself?

No. Since the 2025-06-18 revision the MCP server is only a resource server: it publishes protected resource metadata and validates tokens. Login, consent and token issuance belong to a separate authorization server such as Keycloak, Okta, Entra ID, Auth0 or Cognito.

Is Dynamic Client Registration still allowed in MCP?

Yes, but it is deprecated as of the 2026-07-28 specification and kept only for authorization servers that do not support Client ID Metadata Documents. Clients should prefer pre-registration, then CIMD, then DCR. The deprecation policy promises at least twelve months before removal.

Do stdio MCP servers need OAuth?

No. The spec says stdio implementations should not follow the authorization spec and should take credentials from the environment. The trust boundary for a stdio server is the process that launched it, so sandboxing that process matters more than tokens.

What audience should an MCP access token have?

The MCP server's canonical URI, such as https://mcp.example.com/mcp, which the client sends as the RFC 8707 resource parameter. If your identity provider uses its own audience identifiers, as Entra ID does with client IDs, validate that value instead, but always reject tokens minted for anything else.

This quarter I would do three things on any remote MCP server I run. Check that the audience check exists and rejects a token minted for a different service, because that one test catches the most common real-world mistake. Turn on CIMD at the authorization server and stop adding DCR to new deployments. And trim scopes_supported to the read-only minimum, with write scopes requested through step-up. For the concrete setup, the secure remote MCP server guide walks through Keycloak, the Python SDK and Cloud Run, and what MCP is in the first place covers the protocol basics.

Related: MCP Security in 2026: The Protocol Got Hardened. The Ecosystem Didn't., Who Is This Agent? Non-Human Identity in 2026, From the RFCs to the Breaches and MCP in 2026: 475M SDK Downloads a Month, 39,492 Servers in a Registry Still in Preview, and 12 Gateways Selling the Same 3 Features.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related