Large Language ModelsGenerate imagesGenerate videos
MCP OAuth 2.1 Flow Explained: CIMD vs DCR With Examples
The MCP OAuth 2.1 flow, step by step, from the 401 challenge and metadata lookups to PKCE and token validation. See real JSON for Client ID Metadata Documents and Dynamic Client Registration, a side by side table, and the security checks each approach needs.
An MCP client that wants to call a protected server hits a trust problem on its very first request. The server has never met this client, and the authorization server behind it has never heard of it either. The MCP authorization spec settles this with OAuth 2.1, and the piece that changed most in the past year is how a client gets its client_id. Client ID Metadata Documents (CIMD) arrived in the 2025-11-25 revision as a recommended registration mechanism, and the 2026-07-28 revision marks Dynamic Client Registration (DCR) as deprecated. This article follows the MCP OAuth 2.1 flow from the first 401 response to the first authorized tool call, shows real requests and responses for both registration paths, and ends with a plain rule for choosing between them.
Why MCP Needs OAuth 2.1
Picture a hotel front desk. You show your ID once, the desk confirms who you are, and you walk away with a room card that opens your room and nothing else. OAuth follows the same pattern. The authorization server is the front desk, the access token is the room card, and the MCP server is the door that checks the card. The door never sees your passport, and a card for room 412 does not open room 518.
Authorization is optional in MCP, but the rules turn firm once you switch it on. HTTP based servers SHOULD follow the authorization spec, while stdio servers SHOULD NOT and read credentials from the environment instead. Here is what the spec makes mandatory:
PKCE with the S256 method. Clients must also check that the authorization server advertises code_challenge_methods_supported, and refuse to continue if the field is missing.
Protected Resource Metadata (RFC 9728). The MCP server publishes it, and the client uses it to find the right authorization server.
Resource indicators (RFC 8707). Clients send a resource parameter in both the authorization request and the token request.
Bearer tokens in a header. The Authorization: Bearer header goes on every HTTP request, and tokens never appear in the query string.
Audience validation. An MCP server accepts only tokens that were issued for itself.
The Four Actors
Every flow in this article involves the same four parties. Keep them straight and the rest reads easily.
Actor
OAuth role
Typical example
User
Resource owner
A person approving access in a browser
MCP client
OAuth client
An AI desktop app, an IDE, a CLI agent
MCP server
Resource server
https://mcp.example.com/mcp
Authorization server
Issues tokens
Auth0, Okta, Microsoft Entra ID, or your own service
💡 The MCP server and the authorization server can live in one deployment or belong to two different companies. A client ID is only meaningful to the authorization server that issued or accepted it, so a client must never assume one ID works everywhere.
The Flow From 401 to Token
The handshake is a short chain of plain HTTP requests. You can watch each one in a terminal, which makes debugging far less mysterious than the acronyms suggest.
The 401 Challenge
The client sends an MCP request with no token. The server refuses and tells the client where to look:
The scope parameter is the server's hint about the least privilege needed for this request. If the resource_metadata parameter is absent, the client falls back to well-known URLs, first /.well-known/oauth-protected-resource/mcp (path inserted) and then the root version.
Two Metadata Lookups
The client fetches the Protected Resource Metadata document and reads which authorization server to use:
Then it asks the authorization server to describe itself, trying /.well-known/oauth-authorization-server first and OpenID Connect's /.well-known/openid-configuration second. The issuer inside the response must match the URL the client used to build the request, or the document gets thrown away. A typical answer looks like this:
Two fields in that answer decide how the client registers: client_id_metadata_document_supported (CIMD) and registration_endpoint (DCR). We will come back to both.
PKCE and the Resource Parameter
With a client_id in hand, the client generates a one-time PKCE verifier, hashes it, and opens the browser. It also records the expected issuer so it can check the response later.
GET https://auth.example.com/authorize?response_type=code
&client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient-metadata.json
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&resource=https%3A%2F%2Fmcp.example.com%2Fmcp
&scope=files%3Aread
&state=xyz123
The resource value is the canonical URI of the MCP server. Clients MUST send it even when the authorization server ignores it, because it is what lets a token be bound to one specific server.
Code Exchange and Token Use
The user approves, and the browser lands back on the redirect URI with a code, the state, and ideally an iss parameter. The 2026-07-28 revision adds an issuer check from RFC 9207: if iss is present, the client compares it to the recorded issuer before sending the code anywhere. That blocks mix-up attacks where a hostile authorization server tries to harvest codes meant for an honest one.
Next comes the token request, which proves possession of the PKCE verifier:
The response carries the access token, and from then on every request to the MCP server includes Authorization: Bearer <access-token>. If the token is missing a scope later, the server answers 403 with error="insufficient_scope", and the client re-authorizes with the union of old and new scopes.
DCR: How It Works, Where It Breaks
Dynamic Client Registration comes from RFC 7591. The idea is simple: before the first login, the client sends its details to a registration_endpoint and gets a fresh client_id back. No human fills out a form. For years that was the main way to onboard an unknown client automatically, which is why early MCP adopted it.
A Registration Request
Here is a realistic exchange for a desktop MCP client:
Notice application_type. Since the 2026-07-28 revision, clients MUST set it. Servers that speak OpenID Connect treat a missing value as web, and that default can reject localhost redirect URIs.
Why Servers Struggle With It
DCR works, but it moves a lot of burden onto the authorization server:
A public write endpoint. Anyone on the internet can create records, so you need rate limits, expiry and cleanup jobs.
One registration per pairing. Each client registers separately with each authorization server, and must store the result safely, indexed by issuer. When the authorization server changes, the client has to register again.
Names nobody verified. The consent screen shows whatever client_name the registrant typed, so a hostile app can call itself anything it likes.
Database growth. Thousands of installs of one popular client become thousands of records that all mean the same thing.
Those costs are the reason the spec now points new implementations somewhere else.
CIMD: The URL Is the Client ID
Think of a passport. Nobody asks the border officer to memorize you in advance. You hand over a document, and the officer checks it against the issuing authority. CIMD flips registration the same way. The client publishes a JSON document at a stable HTTPS URL, and that URL is the client_id. The authorization server reads the document when it first sees the URL, so there is nothing to register in advance.
The Metadata Document
The rules for the client are short. The client_id must use https and include a path, the document must contain client_id, client_name and redirect_uris, and the client_id inside the file must match the URL it was served from, character for character. The spec's own example looks like this:
When an authorization request arrives with a URL shaped client_id, the authorization server runs through a fixed routine:
Fetch the document with a plain HTTPS GET.
Confirm it is valid JSON and contains the required fields.
Confirm the client_id in the file equals the URL, exactly.
Confirm the redirect_uri in the request matches one listed in the file.
Cache the result, respecting HTTP cache headers.
Show the user the client_name and the redirect hostname on the consent screen.
A minimal sketch of steps 1 to 3 in TypeScript, written for clarity rather than production use:
async function loadClient(clientId: string) {
const url = new URL(clientId);
if (url.protocol !== "https:" || url.pathname === "/") throw new Error("invalid_client");
await assertPublicHost(url.hostname); // reject private, loopback and link-local addresses
const res = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(5000) });
const doc = await res.json();
if (doc.client_id !== clientId) throw new Error("invalid_client");
if (!doc.client_name || !Array.isArray(doc.redirect_uris)) throw new Error("invalid_client");
return doc;
}
Advertising CIMD Support
The authorization server announces the feature in its metadata with "client_id_metadata_document_supported": true. Clients that find it use their URL as the client_id and skip registration entirely. Because the ID is a public URL, it also travels well: the same client can talk to a different authorization server tomorrow without registering again.
CIMD vs DCR Side by Side
Question
CIMD
DCR
Spec status in 2026-07-28
Recommended (SHOULD)
Deprecated, kept for compatibility (MAY)
Who stores the client record
The client hosts it, the server caches it
The authorization server stores it
Shape of the client_id
A URL such as https://app.example.com/oauth/client-metadata.json
An opaque string such as s6BhdRkqt3
Needs a registration endpoint
No
Yes, advertised as registration_endpoint
Work before the first login
None for the client
One POST per authorization server
Portable across authorization servers
Yes
No, register again per issuer
Main risk
SSRF during the fetch, localhost impersonation
Abuse of an open endpoint, junk records
How a server advertises it
client_id_metadata_document_supported
registration_endpoint
A client that supports every option SHOULD pick in this order:
Use pre-registered client details if it has them for this server.
Use CIMD if the authorization server advertises support.
Fall back to DCR if a registration_endpoint exists.
Ask the user to type in client details by hand.
💡 Pre-registration still wins when you have it. If you control both the client and the authorization server, a fixed client_id skips every lookup above.
Security Checks You Can't Skip
Moving from DCR to CIMD does not remove risk. It moves the risk to different places, and each one needs an owner.
SSRF on the Fetch
With CIMD, an anonymous visitor decides which URL your server requests. Point client_id at https://169.254.169.254/latest/meta-data/ or an internal admin panel, and a careless fetcher becomes a proxy into your network. Resolve the hostname first and reject private, loopback and link-local ranges. Set a short timeout, cap the response size, and be strict about redirects.
Localhost Redirects
A metadata document cannot prove that a process listening on localhost:3000 belongs to the client named in the file. Any local program can claim that port. So the authorization server MUST display the redirect hostname on the consent screen, SHOULD warn when every redirect URI is localhost, and MAY require extra attestation for higher assurance. Redirect URIs themselves must match exactly, never by prefix or pattern.
Audience and Token Passthrough
The MCP server is the last gate, and it must check the card, not just glance at it. Validate the signature, the expiry, the scopes and, above all, the audience. A token minted for another service must be rejected with a 401. If your MCP server calls an upstream API, it needs a separate token for that API, issued by that API's authorization server. Forwarding the client's token is called token passthrough, and the spec forbids it outright.
A short checklist to pin next to your monitor:
Serve every authorization endpoint over HTTPS, and allow only HTTPS or localhost redirects.
Keep access tokens short lived, and rotate refresh tokens for public clients.
Use and verify the state parameter.
Validate iss when it is present, before redeeming the code.
Include all scopes needed for one operation in a single challenge, so the user is not sent through repeated approval screens.
Picking a Strategy
The decision is less dramatic than the debate suggests. Support CIMD first, keep DCR as a bridge, and stay honest about which side of the table you sit on.
If You Run an MCP Server
Publish Protected Resource Metadata no matter what. Choose an authorization server that supports CIMD, and if yours cannot yet, leave DCR switched on with rate limits and expiry rather than blocking every client. Put a scope in your WWW-Authenticate challenge, reply with 403 and insufficient_scope when a token falls short, and verify the audience on every request.
If You Build an MCP Client
Host your metadata document at a URL you will keep for years, because the URL is your identity. Read the authorization server metadata, then choose a registration path in code:
When you do fall back to DCR, store the credentials against the issuer, set application_type: "native" for desktop and CLI apps, and never reuse them with another authorization server.
Try It on PicassoIA
Whichever side of the handshake you build, you will write plenty of JSON, test fixtures and docs. A capable language model speeds that up, and PicassoIA hosts several. Here is a quick way to use Claude Sonnet 5 as a reviewer for your metadata document:
Paste your client-metadata.json and the authorization server metadata from your provider.
Ask for a checklist run: "Check this document against the CIMD rules: client_id equals the URL, https with a path, required fields present, redirect_uris exact. List every failure."
Ask for the output as a table of rule, result and fix, so the answer is easy to paste into a pull request.
For a second opinion, run the same prompt on GPT 5.6 Sol or a quick pass with Gemini 3.5 Flash, and compare the failures each one finds.
💡 Models review documents well, but they do not replace a real test. Run your flow against a staging authorization server before you ship.
Documentation needs pictures as much as it needs JSON. A hero image, a diagram background or a social card makes a post about OAuth far easier to share, and PicassoIA is built for exactly that. Photorealistic results come from specific prompts: name the lens, the direction of the light and the textures in the scene. Try something like "a hotel receptionist sliding a room card across a marble counter, 50mm lens, soft window light from the right, film grain" and see what comes back. Developers can also reach PicassoIA's image and video models through the PicassoIA API and MCP connections, so the same prompt can run from your own agent.
Ready to make your own visuals? Open PicassoIA, pick a model, and start experimenting with your first prompt today.