OAuth for MCP explained

Follow the MCP authorization sequence from a 401 challenge through resource and issuer discovery, PKCE and client registration, token exchange, and retry.

Practitioner10 min readLast reviewed

On this page

The actors

A remote MCP deployment commonly involves four parties: the user, an MCP host/client, the MCP server acting as an OAuth protected resource, and an authorization server that authenticates the user and issues tokens. The MCP server and authorization server may share a domain, but the protocol does not require them to be the same service.

OAuth answers who may access which protected resource with which scopes. It does not decide whether a model should invoke a destructive tool. Clients still need understandable tool descriptions, user consent, and confirmation policy.

The sequence, step by step

1. MCP request and 401 challenge

The client attempts an MCP request without a usable token. The MCP server responds with HTTP 401 Unauthorized and a Bearer WWW-Authenticate challenge. When present, its resource_metadata parameter gives the protected-resource metadata URL; scope can state what the current request needs.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read"

A client must parse the challenge rather than scraping an error body. If resource_metadata is absent, current MCP rules define well-known URL fallbacks based on the MCP endpoint.

2. Protected-resource metadata

The client fetches the RFC 9728 document. Its resource identifies the MCP resource and authorization_servers lists issuers that may authorize access. The client validates URLs and chooses an appropriate issuer; it must not assume the MCP origin is also the issuer.

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://login.example.com"],
  "scopes_supported": ["files:read", "files:write"]
}

3. Authorization-server metadata

The client fetches OAuth Authorization Server Metadata (RFC 8414) and, where applicable, OpenID Connect discovery. It obtains the authorization endpoint, token endpoint, supported PKCE methods, scopes, and client-registration capabilities. Issuer validation matters: accepting metadata or tokens for the wrong issuer enables mix-up and token-redirection attacks.

MCP clients must verify PKCE support and use S256 when technically capable. If the relevant metadata does not advertise a supported code challenge method, the client should stop instead of silently downgrading.

4. Establish client identity

A client ID tells the authorization server which software is asking; it is not proof of the user. Current MCP guidance recognizes these paths, in priority order when supported:

  1. Pre-registered client information already configured for that issuer.
  2. Client ID Metadata Documents (CIMD), where an HTTPS URL serves as the client ID and describes redirect URIs and other metadata. The 2026 specification direction prefers CIMD.
  3. Dynamic Client Registration (DCR) using an advertised registration endpoint. DCR remains a compatibility mechanism and the 2026 release marks it deprecated in favor of CIMD.
  4. Manual client information entered after someone registers the client with the authorization server.

Credentials obtained through DCR or manual registration must be bound to the issuer. Do not reuse a client ID/secret when protected-resource metadata changes to a different authorization server.

5. Authorization code plus PKCE

The client creates a high-entropy code_verifier, derives an S256 code_challenge, stores state, and opens the authorization endpoint. MCP requires the resource parameter in both authorization and token requests so the token is bound to the intended MCP server. The user authenticates and approves scopes.

A simplified authorization request contains:

response_type=code
client_id=https://client.example.com/oauth/metadata.json
redirect_uri=http://127.0.0.1:6276/oauth/callback
code_challenge=<base64url-sha256>
code_challenge_method=S256
state=<unpredictable-value>
resource=https://mcp.example.com/mcp
scope=files:read

The client verifies returned state and, in current protocol guidance, validates issuer information when supplied before redeeming the code. Desktop and CLI clients commonly use a loopback callback. The exact callback must satisfy the authorization server's registration rules.

6. Token exchange

The client sends the code, original verifier, redirect URI, client identity, and resource to the token endpoint. A public desktop or CLI client cannot keep a global embedded secret confidential; PKCE protects the code exchange. A confidential web client can also authenticate with its registered method.

The authorization server returns an access token and may return a refresh token. The client stores them using platform-appropriate secure storage, observes expiration, and rotates refresh tokens when required. Tokens must not be logged or exposed to the model.

7. Authenticated MCP request

The client retries the MCP request with Authorization: Bearer <access-token>. The MCP server validates signature or introspection result, issuer, audience/resource, expiration, and scope. A valid token intended for some other API must be rejected.

If a later request needs more privilege, another 401 challenge may request additional scope. The client should preserve already granted scopes, ask for the minimum increment, and make the step-up visible to the user.

When manual client credentials are actually needed

Manual values are appropriate when an enterprise identity provider requires administrators to pre-register every application; when redirect URIs must be allowlisted before use; when the authorization server supports neither CIMD nor DCR; or when a client/server partnership intentionally uses a pre-agreed confidential client.

They are not a universal prerequisite for MCP OAuth. Asking every user to create an OAuth application is often a sign that automatic client identity is unavailable or misconfigured. Before showing a client-ID form, inspect authorization-server metadata for CIMD support and registration_endpoint, and check whether the client ships issuer-specific registration.

A client secret is especially easy to misuse. A secret distributed inside a desktop binary, CLI package, or public configuration is not secret. Treat such clients as public clients and rely on PKCE unless the provider has a defensible platform-specific mechanism.

Failure map

FailureEvidenceLikely correction
No authorization server foundMissing/invalid resource metadataFix WWW-Authenticate and RFC 9728 well-known routes
PKCE unsupportedMetadata lacks usable code_challenge_methods_supportedConfigure S256; do not downgrade
invalid_redirect_uriRegistration and actual callback differRegister the exact callback; set correct native/web application type
invalid_clientWrong issuer binding, registration, or auth methodRe-register for this issuer; verify client authentication
Code exchange failsState/verifier/redirect mismatch or expired codeKeep transaction state intact; use the original verifier and callback
MCP rejects a fresh tokenWrong resource/audience, issuer, or scopeInclude resource in both requests and validate token claims
Refresh works once onlyRotation not persistedAtomically store the newly returned refresh token
403 from a toolAuthenticated but operation forbiddenInspect application permission and tool result; do not restart OAuth blindly

Log the sequence as redacted events: challenge source, metadata URLs, selected issuer, registration path, requested scopes, callback validation, token status, and authenticated retry. This is enough to debug interoperability without recording codes, verifiers, tokens, secrets, or personal authorization-page data.

Read-only OAuth checks and fixes

The free MCP OAuth checker follows steps 1–5 above without registering a client, sending credentials or opening authorization. Each request has a URL and evidence. A failed path probe can be followed by a successful root or OIDC fallback; judge the validation rows, not an individual fallback's 404. “Clients can start OAuth” means the metadata is ready, not that login or token exchange was verified. Continue with Open in inspector to test a real connection.

For “failed to discover OAuth metadata,” serve public JSON and check the issuer-path rules in the OAuth reference. For “protected resource does not match expected,” fix the published resource to match the MCP audience, using the resource mismatch guide. Do not replace an access-blocked metadata URL with guessed endpoints.

For example, these are the discovery response shapes for https://mcp.example.com/mcp and issuer https://auth.example.com:

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"]
}

Serve that at /.well-known/oauth-protected-resource/mcp (or advertise its location in the 401 challenge). The issuer's /.well-known/oauth-authorization-server should advertise real endpoints and capabilities your server implements:

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none"],
  "code_challenge_methods_supported": ["S256"],
  "client_id_metadata_document_supported": true
}

Only advertise CIMD or a registration_endpoint when implemented. DCR advertising does not prove arbitrary clients will be accepted: Dropbox, Harness and ZoomInfo rejected registration in the recorded sweep under partner/vendor policies. GitHub requires a configured confidential client. These are client-mode restrictions, not automatically missing discovery metadata.

TypeScript SDK applications can serve the challenge from their HTTP auth middleware (replace the example origin with the deployment origin):

res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate');
res.setHeader('WWW-Authenticate',
  'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"');
res.status(401).end();

Python SDK applications can return the same challenge in their ASGI authentication layer:

from starlette.responses import Response

return Response(status_code=401, headers={
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Expose-Headers": "WWW-Authenticate",
    "WWW-Authenticate": 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"',
})

For Cloudflare workers-oauth-provider, check the resource metadata exposed by the wrapper and use an equivalent response for rejected unauthenticated MCP requests:

return new Response(null, { status: 401, headers: {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Expose-Headers': 'WWW-Authenticate',
  'WWW-Authenticate': 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"',
} });

These snippets describe HTTP responses, not complete authorization implementations. Keep PKCE validation, audience enforcement and token validation in the auth provider. See the Cloudflare deployment guide or the integration guides for Auth0, Clerk, WorkOS and Stytch.

Redirect allow-lists are not discoverable metadata. Compare the callbacks in the checker with the provider's configuration, including Claude, ChatGPT's callback-specific variant, Cursor's hosted callback and both VS Code loopback and vscode.dev callbacks. Follow the VS Code redirect guide; exact callback acceptance remains unverified until connection.