In this article
September 3, 2026
September 3, 2026

What is MCP authorization? How OAuth works for AI agents

MCP authorization is the OAuth 2.1 flow that lets an AI agent call a protected MCP server on a user's behalf. Here is how it works, step by step, under the 2026-07-28 spec.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

MCP authorization is the OAuth 2.1 flow that lets an AI agent call a protected MCP server on behalf of a user, without ever holding that user's password or a long-lived API key. In it, three roles map cleanly onto OAuth: the MCP server is the resource server, the MCP client (the agent or its host application) is the OAuth client, and a separate authorization server handles the user interaction and issues short-lived, audience-bound access tokens scoped to that one MCP server.

This article describes the 2026-07-28 revision of the MCP specification, which is current as of September 2026. If you have read about MCP auth before, the change most published guides have not caught up with is that Dynamic Client Registration is now deprecated in favor of Client ID Metadata Documents. Two requirements that are not new but are still widely omitted: the resource parameter from RFC 8707 (a MUST on both authorization and token requests since 2025-06-18) and audience validation on the server side.

Key takeaways

  • Authorization in MCP is OPTIONAL and transport-dependent. HTTP-based servers should implement it; STDIO servers should not, and take credentials from the environment instead.
  • The MCP server is an OAuth 2.1 resource server, not an authorization server. It validates tokens; it does not issue them. Discovery is how the client finds out who does.
  • Tokens are bound to one audience. A client MUST send resource (RFC 8707) identifying the target server, and the server MUST reject any token not issued for itself. This is what stops a token minted for one MCP server from being spent at another.
  • Client registration no longer needs a registration endpoint. Under Client ID Metadata Documents (CIMD), the client_id is an HTTPS URL that serves the client's metadata as JSON. DCR is deprecated and retained only for backwards compatibility.
  • Permissions escalate mid-task via step-up authorization. A 403 with error="insufficient_scope" tells the agent exactly which scopes to go get, and the client must request the union of old and new scopes so it doesn't lose what it already had.

MCP authorization vs. MCP authentication

These get used interchangeably and shouldn't be. Authentication establishes who is calling. Authorization establishes what that caller may do. MCP's authorization spec covers the handshake that produces a scoped access token (the identity question) and then stops.

What it deliberately does not cover is the decision that happens after a valid token arrives: whether this caller may invoke delete_repository on that resource. The spec gives you the scope as a coarse-grained input to that decision and leaves the policy to you. In practice that gap is where most of the design work ends up, not in the handshake.

How the OAuth roles map to MCP

OAuth 2.1 roles and their MCP counterparts.
OAuth 2.1 role MCP component Responsibility
Resource server MCP server Serves tools/resources. Validates every token, checks the audience, returns 401/403 challenges. MUST implement Protected Resource Metadata.
Client MCP client (the agent, or the host app running it) Discovers the authorization server, obtains a client ID, runs the PKCE flow, sends resource, attaches the bearer token to every request.
Authorization server Separate service (may be co-hosted) Authenticates the user, renders consent, issues and refreshes tokens. Its implementation is explicitly out of scope for MCP.
Resource owner The human user Grants or denies consent in a browser.

The important structural point: the MCP server and the authorization server are distinct roles, even when one deployment happens to play both. Earlier revisions treated the MCP server as its own authorization server; the 2025-06-18 revision reclassified MCP servers as OAuth resource servers, and that is the model today. A modern MCP server delegates to an authorization server and confines itself to validation.

When does MCP authorization apply?

Only to HTTP transports. The spec is explicit:

  • HTTP-based transports SHOULD conform to the authorization spec.
  • STDIO transports SHOULD NOT. A local server launched as a subprocess retrieves credentials from its environment. An API key in an env var is the correct pattern there, not OAuth.
  • Other transports MUST follow security best practices for their protocol.

So if you're running a local MCP server on your laptop and wondering why you never encountered an OAuth flow: you're on STDIO, and you're not supposed to.

How MCP authorization works, step by step

The full flow, in the order it actually executes:

1. The unauthenticated request → 401

The client calls the MCP server with no token. The server responds 401 Unauthorized with a WWW-Authenticate header pointing at its Protected Resource Metadata, and, per RFC 6750 §3, SHOULD include the scope the operation requires:

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

That scope hint is what keeps agents from requesting the world on first contact. Clients MUST treat it as authoritative for the current operation.

2. Protected Resource Metadata discovery (RFC 9728)

The client fetches the metadata document. MCP servers MUST implement this; clients MUST use it for authorization server discovery. The real RFC 9728 field names, worth stating plainly because published examples get them wrong:

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

The document MUST include authorization_servers with at least one entry, and it's an array. scopes_supported is meant to be the minimal set needed for basic functionality, not a catalogue of every scope you support; extra permissions get requested incrementally later. Note also that clients MUST NOT assume any set relationship between the scopes named in a WWW-Authenticate challenge and scopes_supported. The challenge is authoritative for the current operation either way.

Clients use the resource_metadata URL from the WWW-Authenticate header when it's present, and MUST otherwise fall back to constructing the well-known URIs directly. A server only has to implement one of the two mechanisms, so a client that skips the fallback will fail against conformant servers.

3. Authorization server metadata discovery

The client fetches the AS's own metadata. Authorization servers MUST provide at least one of OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery 1.0; clients MUST support both and try them in priority order. Three client obligations here that are easy to miss:

  • The issuer in the returned document MUST be identical to the issuer identifier used to build the well-known URL. If they differ, the client MUST NOT use the metadata.
  • The client MUST record that validated issuer value and bind it to the same per-request record holding the PKCE code verifier and state. Step 6's protection is worthless if the expected issuer came from an unvalidated source.
  • The client MUST verify PKCE support before proceeding, MUST use S256 where technically capable, and MUST refuse to proceed if code_challenge_methods_supported is absent from the metadata.

4. Client registration

The agent needs a client_id, and it has never met this server before. Three mechanisms, plus a last-resort fallback. Clients that support all of them SHOULD use this priority order:

  1. Pre-registration, if the client already has credentials for this AS.
  2. Client ID Metadata Documents (CIMD): the default for the common case of no prior relationship. Check for client_id_metadata_document_supported: true in the AS metadata.
  3. Dynamic Client Registration (RFC 7591): deprecated, fallback only. If you do use it you MUST specify an appropriate application_type ("native" for desktop, mobile, CLI and localhost apps; "web" for remote browser-based ones). Omitting it defaults to "web" under OIDC, which collides with native redirect URIs.
  4. Prompt the user to paste credentials, if nothing else works.
Side-by-side comparison of two MCP client registration mechanisms. On the left, Dynamic Client Registration, marked deprecated in the 2026-07-28 spec: the MCP client sends POST /register down to the authorization server, which writes into a persisted client registry. It requires a public write endpoint on the authorization server and leaves one stored record per client per authorization server. On the right, Client ID Metadata Documents, the current default: the MCP client hosts a client-metadata.json file, passes its HTTPS URL as the client_id, and the authorization server fetches that metadata URL back from the client and caches the response according to HTTP cache headers. Nothing is persisted on the authorization server, and the resulting client identity is portable across every authorization server.

One rule that applies to options 1 and 3: client credentials are bound to the authorization server that issued them. Clients MUST key persisted credentials by issuer, MUST NOT reuse them with a different AS, and MUST re-register when the AS changes.

Under CIMD, the client hosts a JSON document at an HTTPS URL and uses that URL as its client_id:

  
{
  "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 authorization server detects the URL-formatted client_id, fetches the document, and validates that the document's own client_id matches the URL exactly and that the requested redirect_uri appears in redirect_uris. That exact-match check is what prevents client impersonation.

The spec doesn't give a rationale for the deprecation, but it does name one concrete advantage: CIMD client IDs are portable across authorization servers, "since they are self-hosted HTTPS URLs resolved by the authorization server on demand. No re-registration is needed when the authorization server changes." The commonly cited operational argument is that DCR obliges every authorization server to expose a public write endpoint that mints and persists a registration record, which scales badly when one popular AI client meets thousands of unfamiliar servers. CIMD inverts that to fetch-and-cache.

5. Authorization request with PKCE and resource

The client generates PKCE parameters and opens a browser:

  
GET /authorize?
  response_type=code
  &client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient-metadata.json
  &code_challenge=E9Melhoa2...&code_challenge_method=S256
  &redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback
  &scope=files%3Aread
  &resource=https%3A%2F%2Fmcp.example.com
  

PKCE is mandatory for all clients under OAuth 2.1, including confidential ones. The resource parameter is the part people miss: RFC 8707 requires it in both the authorization request and the token request, it MUST be the MCP server's canonical URI, and clients MUST send it whether or not the authorization server supports it.

Canonical URI means an absolute URI with no fragment: https://mcp.example.com/mcp, https://mcp.example.com, https://mcp.example.com:8443 and https://mcp.example.com/server/mcp are all valid; mcp.example.com (no scheme) and https://mcp.example.com#fragment are not. Clients SHOULD give the most specific URI they can. The canonical form uses lowercase scheme and host, but implementations SHOULD accept uppercase for robustness. Prefer no trailing slash, and note that this choice has to match the aud claim your server later checks, byte for byte.

6. Issuer validation on the callback (RFC 9207)

The user consents and the AS redirects back with a code. Authorization servers SHOULD include an iss parameter; where one is present, the client MUST validate it against the issuer it recorded in step 3 before sending that code anywhere:

RFC 9207 §2.4 client behavior, by metadata advertisement and iss presence.
authorization_response_iss_parameter_supported iss present? Client action
true yes Compare to recorded issuer (simple string comparison)
true no Reject the response
false or absent yes Compare to recorded issuer
false or absent no Proceed

Order matters: form-decode the iss value out of the application/x-www-form-urlencoded response first, then compare it with no further normalization. Clients MUST NOT apply scheme or host case folding, default-port elision, trailing-slash, or percent-encoding normalization before comparing. This defends against mix-up attacks, where a malicious AS tricks a client into redeeming a code at the wrong token endpoint. It applies to error responses too: on mismatch, the client must not even display the error_description.

7. Token exchange, then the authenticated call

The client redeems the code with code_verifier and resource, gets an access token, and attaches it to every subsequent request:

  
GET /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
  

Authorization MUST be included in every HTTP request. This has been the rule since 2025-06-18, where it was phrased "even if they are part of the same logical session". 2026-07-28 removed protocol-level sessions entirely, so the caveat is simply moot now. Tokens MUST NOT appear in query strings.

Sequence diagram of the MCP authorization flow across four participants: the user-agent browser, the MCP client acting as OAuth client, the MCP server acting as resource server, and the authorization server. Step 1: the client sends an MCP request with no token and receives 401 with a WWW-Authenticate header. Step 2: the client fetches the Protected Resource Metadata document and receives the authorization_servers field. Step 3: the client fetches authorization server metadata and records the issuer. Step 4: the client presents an HTTPS metadata URL as its client_id, and the authorization server fetches and validates that document. Step 5: the client opens the browser to the authorization endpoint with a code challenge and resource parameter, the user consents, and the server returns a code plus iss. Step 6: the browser hands the callback to the client, which validates iss against the recorded issuer per RFC 9207. Step 7: the client exchanges the code with its verifier and resource parameter, receives an access token whose audience is the MCP server, calls the MCP server with a bearer token, and the server validates signature, expiry, issuer and audience before responding.

Token validation and audience binding

This is the part that matters most and is covered least. On receiving a token, the MCP server MUST validate it per OAuth 2.1 §5.2 and MUST confirm the token was issued specifically for itself as the intended audience. The spec then adds three requirements:

"MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server. MCP servers MUST only accept tokens that are valid for use with their own resources. MCP servers MUST NOT accept or transit any other tokens."

The attack this closes is what the spec calls access token privilege restriction. Without an audience check, an agent holding a valid token for Server A can present it to Server B; if B accepts any well-formed token from the same issuer, B acts on A's authority. Related and separately forbidden: a server that forwards a token it received upstream ("token passthrough") launders the audience entirely. As the spec puts it, "the MCP server MUST NOT pass through the token it received from the MCP client."

Diagram showing token audience binding. An authorization server issues an access token whose aud claim is mcp-a.example.com. Presented to MCP server A, whose canonical URI is mcp-a.example.com, the audience matches and the request is served with 200. When the same token is replayed against MCP server B, whose canonical URI is mcp-b.example.com, the audience does not match and server B rejects it with 401. MCP servers must not accept or transit any other tokens.

The spec's distinct confused deputy section covers a different case worth knowing about: an MCP proxy using a static client ID MUST obtain user consent for each dynamically registered client before forwarding to a third-party authorization server, or a stolen authorization code can ride the proxy's identity.

If your authorization server issues JWT access tokens (RFC 9068), validation looks like this, using jose and no vendor SDK:

  
import { createRemoteJWKSet, jwtVerify } from 'jose';

// Derive this from the jwks_uri in the AS metadata you fetched in step 3,
// rather than hardcoding it.
const JWKS = createRemoteJWKSet(new URL(asMetadata.jwks_uri));

const CANONICAL_URI = 'https://mcp.example.com'; // must match `resource` exactly

class Unauthorized extends Error {}

export async function validateToken(authHeader) {
  // The auth-scheme token is case-insensitive per RFC 7235.
  const match = /^Bearer (.+)$/i.exec(authHeader ?? '');
  if (!match) throw new Unauthorized('missing bearer token');

  const { payload } = await jwtVerify(match[1], JWKS, {
    issuer: asMetadata.issuer,
    audience: CANONICAL_URI,        // ← the audience check. Not optional.
    algorithms: ['RS256'],          // allowlist; never trust the header's alg
    requiredClaims: ['exp', 'aud', 'iss', 'sub'],
  });

  // RFC 9068 uses space-delimited `scope`; Entra ID and Okta often emit `scp`.
  const raw = payload.scope ?? payload.scp ?? '';
  const scopes = Array.isArray(raw) ? raw : raw.split(' ').filter(Boolean);

  return { subject: payload.sub, scopes };
}
  

Two things to note. requiredClaims is doing real work: jose validates exp when the claim is present but does not demand it, so a token minted without an expiry would otherwise sail through. And aud matching is exact with no normalization: if your AS mints aud: "https://mcp.example.com/" and CANONICAL_URI omits the trailing slash, every request 401s.

If your authorization server issues opaque tokens instead, none of the above applies. MCP requires validation per OAuth 2.1 §5.2, which RFC 7662 token introspection satisfies equally. Either way, everything past validation is yours to build: which scopes gate which tools.

Error handling

Status codes an MCP server MUST return for authorization errors.
Status Meaning When
401 Unauthorized No token, or the token is invalid or expired
403 Forbidden Valid token, insufficient scope or permissions
400 Bad Request Malformed authorization request

Step-up authorization: when an agent needs more permission mid-task

An agent authorized for files:read tries to write. The server SHOULD respond:

  
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                         scope="files:write",
                         resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         error_description="File write permission required for this operation"
  

The client then:

  1. Parses the error and the required scopes from the header.
  2. Computes the union of scopes it previously requested and the scopes in this challenge. This step is not optional bookkeeping. Servers are only required to name the scopes for the current operation, so a client that re-authorizes with just files:write silently loses files:read. Scope accumulation is a client-side responsibility.
  3. Re-authorizes with that union.
  4. Retries the original request, no more than a few times, then treats it as a permanent failure.

Two things are asked of server authors, at different strengths. Servers SHOULD emit all scopes an operation needs in a single challenge rather than drip-feeding them one round-trip at a time. Incremental challenges force repeated authorization trips for one operation and wreck the user experience. And servers MUST account for scope hierarchies, where a broader scope implies narrower ones, when deciding whether a token is sufficient.

Clients acting for a user SHOULD attempt step-up. Clients acting on their own behalf via client_credentials MAY attempt it or just abort, which points at a real open question below.

Which spec revision are you building against?

What each MCP specification revision changed for authorization.
Revision What changed for authorization
2025-03-26 First authorization spec. OAuth 2.1 + PKCE, DCR as the headline convenience. The revision most published guides still describe.
2025-06-18 MCP servers reclassified as OAuth resource servers. Protected Resource Metadata (RFC 9728) and Resource Indicators (RFC 8707) introduced.
2025-11-25 Client ID Metadata Documents added as a recommended registration mechanism. OIDC Discovery 1.0 added as an AS discovery option. Step-up authorization and scope challenge handling introduced.
2026-07-28 Current. DCR formally deprecated. iss validation (RFC 9207) required. Step-up gains the scope-union requirement and a scope-hierarchy MUST. Client credentials bound to their issuing AS. Refresh token guidance added. Protocol-level sessions removed.

If a guide you're reading mentions DCR without mentioning CIMD, it predates November 2025. If it presents step-up authorization as brand new, it's describing 2025-11-25.

What the spec leaves to you

The authorization spec is deliberately narrow. Four things it does not answer:

  • Who is the resource owner when no user is present? The spec accommodates client_credentials clients but says little about them. An agent running on a schedule with no human to consent is a genuinely unsolved delegation problem, not a solved one.
  • Which tools may this identity call? A valid token with files:write doesn't tell you whether this agent should be writing to this path. That's authorization policy, and it's entirely yours.
  • How do you run an authorization server? "Beyond the scope of this specification", which is accurate and also the largest piece of work in the list. CIMD validation alone means fetching remote documents (with SSRF defenses), caching them on HTTP headers, and exact-matching redirect URIs.
  • How do you keep up? Four revisions in sixteen months, each moving normative requirements.

That last one is why most teams delegate the authorization server rather than build one. AuthKit implements the current spec (CIMD, Resource Indicators, PRM, issuer validation), so your MCP server keeps only the part that has to live in your code: validating the token and deciding what its bearer may do.

FAQ

Is MCP authorization required?No. It's OPTIONAL in the spec. But if your server is remote and touches real data, you need it. And if you implement it over HTTP, you SHOULD conform to the spec rather than invent something.

Do local or STDIO MCP servers need OAuth?No. STDIO implementations SHOULD NOT follow the authorization spec; they take credentials from the environment.

Is Dynamic Client Registration still the right way to register clients?No. As of 2026-07-28, DCR is deprecated in favor of Client ID Metadata Documents and retained only for backwards compatibility with authorization servers that don't support CIMD yet. New implementations should use CIMD.

What goes in the resource parameter, and why?The canonical HTTPS URI of the MCP server you intend to call. It tells the authorization server which audience to bind the token to, so the token can't be replayed against a different server. Required in both the authorization and token request, and required whether or not your AS supports it.

How does an MCP server verify a token was issued for it?Validate the aud claim against the server's own canonical URI. Reject anything else, and never forward a received token upstream.

Does the MCP server issue its own tokens?It shouldn't. It's a resource server. Earlier spec revisions pushed servers toward minting tokens bound to a third-party session; that guidance is gone.

Why is PKCE mandatory?OAuth 2.1 requires it for all clients, including confidential ones, because it binds the authorization code to the client that requested it and defeats code interception on the redirect.

What happens when an agent needs more permissions mid-task?The server returns 403 with error="insufficient_scope" and the required scopes. The client re-authorizes with the union of old and new scopes, then retries.

How do refresh tokens work for MCP clients?Clients that want them SHOULD include refresh_token in their grant_types metadata and MAY add offline_access to the scope parameter when the AS advertises it, but MUST NOT assume refresh tokens will be issued, since the AS retains discretion. MCP servers SHOULD NOT advertise offline_access in WWW-Authenticate or scopes_supported; refresh is a client concern, not a resource requirement.

Is MCP authorization the same as MCP authentication?No. Authentication is who's calling; authorization is what they may do. The spec covers the token handshake and leaves per-tool policy to you.

Which MCP spec revision should I build against?2026-07-28. Confirm any guide you follow names its revision; several widely-cited explainers still describe 2025-03-26.