·MCP Servers
How MCP authorization works in the 2025-11-25 spec: Protected Resource Metadata discovery, Client ID Metadata Documents vs dynamic registration, RFC 8707 resource binding, and step-up authorization — plus symptom-driven OAuth debugging and three end-to-end integration walkthroughs. Updated Sep 2026.
MCP Authorization: OAuth 2.1, CIMD, and Step-Up Flow Done Right (2026)
MCP authorization is the OAuth 2.1 framework the Model Context Protocol defines for HTTP-based transports: the MCP server acts as an OAuth resource server that protects its endpoints with access tokens, while stdio local transports SHOULD NOT use this flow at all. The 2025-11-25 spec revision rewrote client registration — Client ID Metadata Documents (CIMD) became the primary path and Dynamic Client Registration (DCR) was demoted to a compatibility option — which means most MCP OAuth tutorials written in 2025 now describe a stale priority order. This guide walks the current authorization spec end to end: the discovery chain, the three registration mechanisms, the full PKCE + resource parameter flow, server-side validation duties, and step-up authorization. (New to MCP? Start with what is MCP; for choosing between transports see the MCP transports comparison.)
TL;DR
- An MCP server is a resource server in OAuth 2.1 terms; stdio transports take credentials from the environment and skip this machinery
- Discovery chain:
401 + WWW-Authenticate→ Protected Resource Metadata (RFC 9728, MUST for MCP servers) → authorization server metadata (RFC 8414 / OIDC Discovery — clients MUST support both)- The 2025-11-25 change that matters: CIMD is now the primary registration method (SHOULD); DCR is downgraded to MAY, kept for backwards compatibility
- The
resourceparameter (RFC 8707) MUST be included in both authorization and token requests — it binds tokens to the target MCP server- MCP servers MUST validate token audience and MUST NOT pass client tokens through to upstream APIs
- Runtime permission shortfalls use step-up:
403 + insufficient_scope→ re-authorize with new scopes → retry with a limit
What changed in the 2025-11-25 spec
The authorization spec has gone through three published revisions, and client registration changed the most — that's exactly why older tutorials mislead now:
| Mechanism | 2025-06-18 spec | 2025-11-25 spec |
|---|---|---|
| Dynamic Client Registration (DCR, RFC 7591) | SHOULD support (the de-facto main path) | MAY support, kept for backwards compatibility |
| Client ID Metadata Documents (CIMD) | Did not exist | SHOULD support; first choice when there's no prior relationship |
| Protected Resource Metadata discovery | WWW-Authenticate header only | Header + well-known URI fallback (clients MUST support both) |
| Authorization server metadata | RFC 8414 only | RFC 8414 and OIDC Discovery; clients MUST support both |
| Step-up authorization | Not formally defined | Formal flow: 403 + insufficient_scope → re-auth → retry cap |
If you built an MCP client in 2025 that starts with "POST /register to get a client_id", it still works under the new spec (DCR remains a legitimate fallback), but the primary path has moved: clients should check client_id_metadata_document_supported in the authorization server metadata first, and use CIMD when it's true.
Why remote MCP servers need OAuth
MCP leaves authorization optional (the spec marks it OPTIONAL), but sets the ground rules: when supported, HTTP-based transports SHOULD conform to the spec. The credential models of the two transports don't overlap:
| Dimension | stdio (local process) | Streamable HTTP (remote) | |---|---|---| | Credentials come from | Environment variables (e.g. API keys) | OAuth 2.1 access tokens | | Spec's position | SHOULD NOT use this authorization spec | SHOULD conform to it | | Typical threats | Local process over-reaching into files | Token theft, cross-service replay, confused deputy | | User presence | No browser available | Browser available for the authorization code flow |
The fundamental shift in the remote case: the MCP server no longer runs on the user's machine, and it must decide on behalf of a resource owner whether a request is legitimate — precisely the resource-server role in OAuth 2.1. The spec also pins the transport baseline: all authorization server endpoints MUST be served over HTTPS, and redirect URIs MUST be either localhost or HTTPS.
The discovery chain: from 401 to the authorization server
A client connecting to a protected MCP server for the first time starts with no configuration at all — the whole mechanism bootstraps from a single 401, in four steps:
- The client requests the MCP endpoint without a token; the server replies
401 Unauthorizedwith aWWW-Authenticateheader carrying theresource_metadataURL; - The client fetches that URL and receives the Protected Resource Metadata (RFC 9728), whose
authorization_serversfield lists one or more authorization servers; - The client probes the authorization server metadata endpoints in priority order (for issuers with a path:
/.well-known/oauth-authorization-server/<path>→/.well-known/openid-configuration/<path>→/<path>/.well-known/openid-configuration; for path-less issuers: the first two at the root); - With metadata in hand, the client runs the OAuth 2.1 authorization code flow with PKCE.
A 401 looks like this (the spec's own example):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
Two implementation details bite people: first, the scope parameter in the WWW-Authenticate header is the first-priority source when the client picks scopes (see the scope strategy below); second, when the header is absent the client MUST probe well-known URIs in a fixed order — the MCP endpoint's path form first, then the root. A wrong discovery implementation shows up as "some servers connect, some don't".
Client registration: CIMD first, DCR for compatibility
"How does a client get a client_id" is where the 2025-11-25 revision changed the most. MCP defines three registration mechanisms, and clients supporting all three SHOULD pick them in this priority order:
| Mechanism | When it applies | Spec position (2025-11-25) |
|---|---|---|
| Pre-registration | Client and server have an existing relationship | Priority 1: use it when available |
| Client ID Metadata Documents | No prior relationship (the common case) | Priority 2: use when the server metadata declares client_id_metadata_document_supported: true |
| Dynamic Client Registration (DCR, RFC 7591) | Backwards compatibility or special needs | Priority 3: fallback; only when the server metadata exposes a registration_endpoint |
CIMD's core idea: use an HTTPS URL as the client_id. The client hosts a JSON metadata document on its own domain; when the authorization server sees a URL-shaped client_id, it fetches, validates, and displays it:
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"redirect_uris": [
"http://127.0.0.1:3000/callback",
"http://localhost:3000/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
Hard validation rules: the document's client_id MUST match its URL exactly; the client_id MUST be an HTTPS URL with a path component; the authorization server MUST check that the redirect URI in the authorization request appears in the document's redirect_uris. The required fields are client_id, client_name, and redirect_uris.
CIMD brings its own security surface: authorization servers must think about SSRF before fetching arbitrary URLs, and CIMD cannot prevent localhost impersonation — an attacker can present a legitimate client's metadata URL, bind to any local port, and receive the authorization code. That's why the spec recommends extra warnings for localhost-only redirect URIs, and requires servers to MUST prominently display the redirect URI hostname on the consent screen.
The authorization flow: PKCE, the resource parameter, and scope strategy
All three registration mechanisms funnel into the same authorization code flow. The complete sequence:
- Generate PKCE parameters (a
code_verifierand itsS256code_challenge); - Open the browser with the authorization request, including the
code_challengeandresourceparameters; - The user authenticates and consents at the authorization server, which redirects the authorization code to the registered
redirect_uri; - The client exchanges the code for tokens using the code, the
code_verifier, and theresourceparameter (optionally receiving a refresh token); - The client calls the MCP server with an
Authorization: Bearer <token>header — on every request, including follow-ups within the same logical session; tokens MUST NOT appear in the URI query string.
PKCE is a MUST, and clients MUST use the S256 method. The spec adds one defensive requirement on top: clients MUST verify that code_challenge_methods_supported exists in the server metadata before proceeding — if the field is absent, the server doesn't support PKCE and the client MUST refuse to continue rather than silently downgrade to a bare code flow.
The resource parameter comes from RFC 8707 (Resource Indicators) and binds the token to the target MCP server: both the authorization request and the token request MUST carry it, set to the MCP server's canonical URI — lowercase scheme and host, optional port, no fragment — such as https://mcp.example.com or https://mcp.example.com/server/mcp, and by convention without a trailing slash. Clients MUST send it regardless of whether the authorization server advertises support — audience binding depends on it.
Scope selection has its own priority: use the scope parameter from the 401's WWW-Authenticate header first; fall back to scopes_supported from the Protected Resource Metadata (and omit the scope parameter entirely if even that is undefined). Request the minimal set up front and grow via step-up — least privilege, and no eight-checkbox consent screen on first contact.
Server-side duties: audience validation and the token passthrough ban
The MCP server's (resource server's) hard obligations are the highest-security-density part of the spec:
- Validate the audience on every token. The server MUST check, per RFC 8707, that the token was issued specifically for it; wrong audience or an expired token MUST get a 401. Signature checking without audience validation is no boundary at all — a legitimately signed token minted for another service walks right in.
- No token passthrough. If the MCP server calls an upstream API (a GitHub API backing a git tool, say), it is a separate OAuth client toward that API and must obtain its own token from the upstream authorization server. Forwarding the client's token upstream is a MUST NOT. This is the confused deputy problem: the downstream service mistakes the passed-through token for the MCP server's own identity, and both the audit trail and the permission boundary collapse.
- Proxies with static client IDs: an MCP proxy MUST obtain user consent for each dynamically registered client before forwarding to third-party authorization servers.
- Error-code conventions: 401 = unauthenticated or invalid token, 403 = insufficient scope, 400 = malformed request.
The security best practices document ranks token passthrough as the top anti-pattern and prescribes the same triangle this guide does: audience validation + the resource parameter + no passthrough.
Step-up authorization at runtime
The user already consented, the token is valid — but an operation needs a permission it doesn't cover. The 2025-11-25 spec defines a formal step-up flow for this. The server answers 403 Forbidden with a WWW-Authenticate header that says exactly what's missing:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="files:read files:write user:profile",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
error_description="Additional file write permission required"
The client's standard response: parse the error → determine the new scope set per the scope strategy → run a new authorization round → retry the original request with the fresh token. Two guardrails belong in every implementation: retries must be capped (the spec says "no more than a few times" — treat a further failure as a permanent authorization failure), and clients acting for a user SHOULD attempt step-up while client_credentials background clients MAY abort instead. Uncapped retries turn one misconfigured scope into an authorization loop storm.
Implementation checklist: what to verify before shipping
Server-side self-check, in spec order:
- RFC 9728 Protected Resource Metadata is implemented, with an
authorization_serversfield; - 401 responses carry
WWW-Authenticate: Bearer resource_metadata="..."plus ascopehint; - Token validation checks both signature and audience; upstream calls use independently obtained tokens, never passthrough;
- The consent screen displays the client name and the redirect URI hostname in full.
Client-side self-check:
WWW-Authenticateis parsed, with ordered well-known URI fallback;- Authorization server metadata resolution supports both RFC 8414 and OIDC Discovery (three-level priority for path-bearing issuers);
- PKCE uses S256, and
code_challenge_methods_supportedis verified to exist first; - Both the authorization and token requests carry
resource; every API request carries the Bearer header; - The step-up flow has a retry cap.
Test with the official MCP Inspector — it implements the full discovery and authorization chain and will walk your server through 401 → metadata → authorization code → authenticated call. For the server's mcpServers config itself see the MCP servers configuration guide; when connections fail, the MCP debugging playbook covers the usual suspects. Authorization extensions are versioned separately in the modelcontextprotocol/ext-auth repository — optional, composable, and independent of the core spec's release cycle.
Common Pitfalls and Anti-Patterns: Diagnosing OAuth Bugs from Symptoms
The checklist above goes item by item before launch; this section works backwards — from the symptom you are seeing to the requirement you missed. These five symptoms cover most real-world OAuth integration failures (spec text: the MCP authorization spec).
Symptom 1: Authorization succeeds, but every API call returns 401 invalid_token. Symptom: you have the token, the Bearer header is set, and the request still fails. Cause: the resource parameter differed between the authorization request and the token request (trailing slash, case, path), or was missing from one of them — the issued token's audience does not match the server's own URI, and the resource server must reject it. Fix: use the exact same canonical URI in both places (lowercase scheme/host, no fragment, no trailing slash by convention), and log audience validation failures with both the expected and actual values.
Symptom 2: Some servers connect, others don't. Symptom: your client sails through discovery with server A but stalls on server B. Cause: discovery is hard-coded to one path, or you implemented RFC 8414 without OIDC Discovery — the current spec says both MUST be supported, and path-bearing issuers need the three-level fallback order. Fix: implement the full fallback chain: WWW-Authenticate first, then the well-known URIs in the order described in the discovery-chain section above.
Symptom 3: Silent downgrade to no PKCE. Symptom: a security audit finds bare authorization-code requests in production traffic. Cause: the client never checked code_challenge_methods_supported in the server metadata and fell into the no-PKCE branch when the field was absent. Fix: missing field = server does not support PKCE = the client MUST refuse to continue — abort with an error instead of downgrading. An intercepted authorization code costs far more than a temporarily unreachable server.
Symptom 4: redirect_uri mismatch — or worse, too loose. Symptom: everything works locally, then production throws redirect_uri_mismatch; or, the other way, someone "simplifies" matching and a security ticket follows. Cause: the registered and requested redirect_uri are not identical (trailing slash, port, localhost vs 127.0.0.1 are all different URIs). Fix: store the registered URI as a constant and reuse it verbatim; loopback redirects may vary the port per RFC 8252, but production domains must match exactly — no wildcards.
Symptom 5: Tokens show up in URLs or logs. Symptom: an audit finds access tokens in gateway access logs, or worse, in upstream URL query strings. Cause: the token was spliced into a URI, or logging printed the full Authorization header — the spec says tokens MUST NOT appear in URIs. Fix: tokens travel only in the Bearer header; redact Authorization and Cookie headers in logs; add an alert for token-shaped strings appearing in URIs.
Real-World Walkthroughs: Three Integration Scenarios, Start to Finish
Case A: A client connecting to a protected MCP server. Scenario: you are handed https://mcp.example.com/mcp with zero pre-configuration. Steps: send a bare request and receive the 401 → parse the resource_metadata URL from WWW-Authenticate → fetch the RFC 9728 document and read authorization_servers → probe authorization-server metadata in the three-level order → confirm client_id_metadata_document_supported, then host a client metadata document to register → run the authorization-code flow with PKCE and resource → attach the Bearer header to every request. Verify against MCP Inspector throughout — its implementation is the reference answer for this exact flow.
Case B: Wrapping an internal REST API as a protected MCP server. Scenario: the company already has an order API with in-house OIDC (say Keycloak), and you are adding an MCP layer. Steps: implement the Protected Resource Metadata document with authorization_servers pointing at Keycloak; return WWW-Authenticate plus a scope hint on every 401; add an audience assertion to token validation (the token was issued to this server, not just correctly signed); show the client name and redirect_uri hostname on the consent screen. With those four pieces in place, any compliant MCP client can connect with zero configuration.
Case C: A git tool that calls the GitHub API — the two-token architecture. Scenario: your MCP server wraps git operations and needs to call the GitHub API as the user. The wrong way: pass the client's token straight through to GitHub — the spec MUST NOTs this; it is the confused-deputy problem. The right way: the server acts as its own OAuth client toward GitHub (its own client_id, its own scope), managed, rotated, and revoked independently of the MCP client token. Two tokens, two trust chains, and a clean audit boundary (see the security best practices).
Frequently asked questions
Do stdio MCP servers need OAuth?
No. The spec says stdio transports SHOULD NOT use this authorization flow — a local process takes credentials from environment variables (API keys and the like), and there's no browser to complete an authorization code flow anyway. The OAuth machinery targets remote MCP servers over HTTP transports, which run as resource servers.
What are OAuth Client ID Metadata Documents (CIMD)?
A client registration method where the client uses an HTTPS URL as its client_id; the URL points to a JSON metadata document hosted on the client's domain (with client_id, client_name, redirect_uris, and friends). When an authorization server sees a URL-shaped client_id, it fetches and validates the document. It solves MCP's most common scenario — client and server have no prior relationship, so there's nothing to pre-register against. As of the 2025-11-25 spec, clients and authorization servers SHOULD support CIMD.
Why are 2025 MCP OAuth tutorials outdated now?
Because the 2025-11-25 revision rewrote registration priorities: Dynamic Client Registration dropped from SHOULD to MAY and is kept only for backwards compatibility, while CIMD became the first choice for the no-prior-relationship case. Older tutorials teach "POST /register first"; the new spec has clients check client_id_metadata_document_supported in the authorization server metadata first. The well-known URI fallback order and step-up authorization are also new in this revision.
Is the resource parameter mandatory? What breaks without it?
Yes. The spec requires MCP clients to implement RFC 8707 and include the resource parameter in both the authorization and the token request, set to the MCP server's canonical URI (no trailing slash, no fragment). It underpins audience binding: servers MUST verify the token was issued specifically for them. Without resource, tokens can be minted as generic-purpose, and cross-service replay risk follows.
Can an MCP server forward my token to an upstream API?
No. Token passthrough is a MUST NOT: when the MCP server calls an upstream API it acts as an independent OAuth client of that API's authorization server and must obtain its own token. Forwarding causes the confused deputy problem — the downstream service trusts the token as if it were the MCP server's identity, and both the permission boundary and the audit trail fail.
What should I do when I get an insufficient_scope error?
That's the step-up authorization entry point: the server returns 403 with WWW-Authenticate carrying error="insufficient_scope", the required scope set, and resource_metadata. The client parses it, runs a new authorization round (existing scopes plus the new ones), and retries the original request with the fresh token. Cap the retries — the spec suggests a few attempts, treating further failure as a permanent authorization error, to avoid an infinite authorization loop.
When an MCP client first hits a protected server, how does it know where to authorize?
From a single 401. The client requests the MCP endpoint without a token; the server replies 401 with a WWW-Authenticate header carrying a resource_metadata URL. Fetching it yields the Protected Resource Metadata (RFC 9728), whose authorization_servers field lists the authorization servers. The client then probes authorization server metadata in the fixed fallback order (RFC 8414 and OIDC Discovery both) and runs the authorization code flow with PKCE.
Is PKCE optional in the MCP OAuth flow, and which code challenge method should be used?
No — PKCE is mandatory, and the S256 method is required. Clients generate a code_verifier and its S256 code_challenge, and must first verify that code_challenge_methods_supported exists in the server metadata. If the field is absent, the server does not support PKCE and the client must refuse to continue rather than silently downgrade to a bare authorization code flow, which would expose the code to interception.
Official references
- MCP Authorization spec (2025-11-25, primary source for this guide)
- MCP Authorization spec (2025-06-18, for comparison)
- MCP Security Best Practices: token passthrough and the confused deputy
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- OAuth Client ID Metadata Documents (IETF draft)
- Stytch: MCP OAuth and Dynamic Client Registration (a 2025 view, useful for the diff)
- modelcontextprotocol/ext-auth: MCP authorization extensions repository
- MCP Inspector: the official debugging tool
This guide reflects the 2025-11-25 MCP authorization spec as of August 2026. The spec keeps evolving — check the latest revision before implementing.