Authorization & Security
The full OAuth 2.1 flow MCP builds on — discovery, PKCE, the resource parameter, issuer validation, and step-up authorization
Authorization & Security
TL;DR
MCP authorization is built on OAuth 2.1, with the MCP server acting as a resource server. It's optional overall, recommended for Streamable HTTP servers that need it, and explicitly not meant for stdio servers, which should pull credentials from the environment instead. The protocol adds two things ordinary OAuth doesn't emphasize as strongly: a mandatory resource parameter that binds every token to exactly one MCP server, and a validated iss check that closes a specific class of token-confusion attack.
| Property | Value |
|---|---|
| Level | Intermediate |
| Reading time | ~22 minutes |
| Prerequisites | Transports |
| You will understand | The full authorization flow, why each step exists, and how to handle runtime scope errors |
Who Does What
Authorization is optional, and transport-dependent. A stdio server SHOULD NOT implement this specification — it should retrieve credentials from the environment (an API key already in a config file or env var) instead. An HTTP-based server that supports authorization at all SHOULD conform to this spec. This isn't a stylistic choice; the two transports answer a fundamentally different question, as covered in Transports.
MCP maps cleanly onto standard OAuth 2.1 roles:
| OAuth role | Who plays it |
|---|---|
| Resource server | The MCP server — the thing being protected |
| Client | The MCP client — making requests on the user's behalf |
| Authorization server | Issues tokens; may be hosted with the resource server, or be a separate identity provider entirely |
Discovery: How a Client Finds the Authorization Server
Before a client can get a token, it has to find out where to get one. MCP servers MUST implement OAuth 2.0 Protected Resource Metadata (RFC 9728) — a well-known document that points a client at its authorization server. The authorization server, in turn, MUST provide at least one of OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery; clients MUST support both, since they can't know in advance which one a given server offers.
Client Registration
A client needs a client_id before it can request tokens. Three mechanisms exist, and MCP has an explicit priority order among them:
Client registration, in priority order
Client ID Metadata Documents
RecommendedThe client uses an HTTPS URL it controls as its client_id. The authorization server fetches a JSON metadata document from that URL (containing things like redirect_uris) to validate the client — no separate registration step required. This is now the preferred mechanism.
Pre-registered client
The client already has a client_id, arranged out-of-band ahead of time. Simple, but doesn't scale to clients the server operator has never heard of.
Dynamic Client Registration
A POST /register call that returns fresh client credentials. This is now deprecated, retained only for backwards compatibility with authorization servers that don't yet support Client ID Metadata Documents.
The Full Authorization Flow
The full authorization flow
Walking through the steps that matter most:
PKCE and the resource parameter
The client generates PKCE parameters (protecting the authorization code from interception) and — this is MCP-specific — MUST include a resource parameter in both the authorization request and the token request, identifying exactly which MCP server the token is for, using its canonical URI.
Valid: https://mcp.example.com/mcp
Valid: https://mcp.example.com
Valid: https://mcp.example.com:8443
Invalid: mcp.example.com (missing scheme)
Invalid: https://mcp.example.com#frag (contains a fragment)This is what stops a token issued for one MCP server being replayed against a different one. MCP servers MUST validate that a presented token's audience matches themselves specifically, and MUST NOT accept a token that wasn't issued for their exact resource. A client MUST NOT send a token to any MCP server other than the one whose authorization server actually issued it. Without the resource parameter, a token is just "valid for whatever accepts it" — with it, a token is bound to one server, provably.
Issuer validation (iss)
After the user authorizes, the redirect back to the client includes the authorization code — and, per RFC 9207, an iss parameter naming which authorization server issued it. The client MUST compare this against the issuer it recorded before opening the browser, using simple string comparison, with no scheme/host case-folding or trailing-slash normalization applied first.
Skipping this check is exactly what a mix-up attack exploits. If a client is talking to more than one authorization server (a realistic scenario when connecting to several MCP servers), and doesn't verify which one actually issued a given code, an attacker can potentially trick the client into sending a code — or accepting a token — from the wrong authorization server. Validating iss against a value recorded before the redirect closes that gap.
The token exchange and using the result
The client exchanges the code, the PKCE code_verifier, and the same resource parameter for an access token (and possibly a refresh token). Every subsequent MCP request carries it as:
GET /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Never in the URI query string. An invalid or expired token gets a plain HTTP 401.
Runtime Scope Errors: Step-Up Authorization
A token that was valid enough to get past login can still be insufficient for a specific operation — reading is one scope, writing might be another. When that happens mid-session, the server responds differently from an outright missing-token 401:
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"Handling a 403 insufficient_scope response
Parse the error
Read the required scope(s) from the WWW-Authenticate header
Compute the union
Combine the newly-required scopes with whatever was already granted — don't discard prior permissions
Re-authorize with the union
A fresh authorization request for the combined scope set
Retry the original request
With a retry limit — repeated failure is treated as a permanent authorization failure, not retried forever
This is deliberately incremental: a client shouldn't have to ask for every conceivable scope up front just in case, but it also shouldn't lose already-granted permissions every time it needs one more.
Refresh Tokens
If a client wants refresh tokens, it MUST keep them confidential in transit and storage, SHOULD declare refresh_token in its grant_types, and MAY request the offline_access scope where the authorization server supports it — but MUST NOT assume one will actually be issued. The authorization server retains full discretion here.
The Broader Security Picture
The OAuth mechanics above answer "is this request authenticated." They don't answer "should this request be trusted" — that's a separate, protocol-level set of concerns MCP names explicitly:
| Threat named in the spec | What it means here |
|---|---|
| Token audience binding/validation | Covered above — the resource parameter and server-side audience checks |
| Token theft | Stolen bearer tokens are usable by anyone who has them — treat them like passwords, not like public identifiers |
| Authorization code protection | PKCE exists specifically to stop an intercepted code from being redeemed by someone other than the client that requested it |
| Mix-up / confused deputy attacks | Covered above via iss validation — and the general shape recurs anywhere a trusted party (the model, via Tools) can be tricked into using its own legitimate authority for an unintended purpose |
| Open redirection | Validate redirect URIs strictly; don't let an attacker redirect a completed auth flow somewhere they control |
Authorization extensions exist for cases the core flow doesn't cover well — an OAuth Client Credentials flow for machine-to-machine access with no human in the loop, and Enterprise-Managed Authorization for centralized, org-wide access control. Both are covered in Extensions.
Concept Checks
Check yourself
Why shouldn't a stdio MCP server implement the OAuth flow described on this page?
Because it runs as a direct subprocess of the host, on the same machine, already operating with the permissions of the user running that host — there's no separate network identity to establish. The spec explicitly says stdio implementations SHOULD NOT follow this authorization specification and should retrieve credentials from the environment instead.
What specifically does the `resource` parameter prevent, and how?
It prevents a token issued for one MCP server being replayed against a different one, by binding every authorization and token request to the canonical URI of the exact server the client intends to use the token with. Servers MUST validate that a presented token's audience matches themselves specifically, so a token scoped to server A is provably useless against server B.
Why does the client need to validate the `iss` parameter against a value it recorded before opening the browser, rather than just trusting whatever comes back?
Because trusting an unvalidated iss value provides no real protection — the check is only meaningful if it's compared against something recorded from a source already known to be authentic. Recording the expected issuer before redirecting, then requiring an exact match on return, is what actually defends against a mix-up attack where a code or token from the wrong authorization server gets accepted.
A client already holds a token with 'files:read' scope and gets a 403 asking for 'files:write' too. What should it request on re-authorization?
The union of both scopes — files:read and files:write together — not just the newly-challenged files:write scope alone. Requesting only the new scope risks losing the previously granted files:read permission if the authorization server treats each grant as replacing the last, which is exactly what the step-up flow's union requirement is designed to avoid.
Key Concepts Recap
| Concept | One-line summary |
|---|---|
| Stdio vs HTTP auth | Stdio uses environment credentials; HTTP-based servers that need auth use this OAuth flow |
| MCP server = resource server | Standard OAuth 2.1 roles, MCP server on the receiving end |
| Protected Resource Metadata (RFC 9728) | How a client discovers which authorization server to use |
| Client ID Metadata Documents | The now-preferred registration mechanism; Dynamic Client Registration is deprecated |
resource parameter (RFC 8707) | Binds a token to one specific MCP server's canonical URI |
iss validation (RFC 9207) | Defends against mix-up attacks by confirming which authorization server actually issued a code |
| Step-up authorization | 403 + insufficient_scope triggers a union re-authorization, not a scope replacement |
| Bearer header only | Tokens go in Authorization: Bearer, never in a URL query string |
Next
With the trust boundary established, the next page turns practical: writing an actual server and client. Building Servers & Clients.