Part 20 of 22

MCP authentication explained: OAuth, scopes, and safe token handling

LLM Mart · Sep 20, 2026 · 4 views 218 listing impressions
MCP authentication explained: OAuth, scopes, and safe token handling

MCP authorization is OAuth 2.1, applied at the transport level, for HTTP-based servers only. A protected MCP server acts as an OAuth resource server, the client acts as an OAuth client, and a separate authorization server issues the tokens. Local stdio servers do not use this at all — they take credentials from the process environment.

Nothing about the flow is novel. What is easy to get wrong is the part that is specific to MCP: binding a token to one server, requesting the narrowest scopes, validating the issuer before you spend an authorization code, and enforcing permissions on the server rather than in the tool list.

When authorization applies

Authorization is optional in MCP. When it is used:

  • HTTP-based transports should conform to the authorization specification.
  • stdio implementations should not. They retrieve credentials from the environment, the way any local process does. There is no OAuth flow because there is no network boundary to authorize across.
  • Other transports must follow established security practice for their own protocol.

If you are choosing between the two, see MCP stdio vs. Streamable HTTP; the credential model is one of the real differences between them.

The flow, end to end

The specification composes a selected subset of established RFCs rather than inventing a scheme. The sequence is:

  1. Unauthenticated request. The client calls the MCP endpoint with no token.
  2. Challenge. The server responds 401 Unauthorized with a WWW-Authenticate header carrying a resource_metadata URL and, ideally, a scope parameter.
  3. Protected resource metadata. The client fetches that metadata document, which names the server's authorization server(s). MCP servers must implement RFC 9728 for this, and clients must use it for discovery.
  4. Authorization server metadata. The client discovers endpoints and capabilities through RFC 8414 or OpenID Connect Discovery. Servers must provide at least one; clients must support both.
  5. Client identity. Via a Client ID Metadata Document, a pre-registered client ID, or dynamic client registration.
  6. Authorization request. With PKCE parameters, the resource parameter, and the selected scopes. The client records the expected issuer before redirecting.
  7. Callback. The client validates the returned iss against the recorded issuer, then exchanges the code with the verifier and the same resource parameter.
  8. Authenticated requests. Authorization: Bearer <token> on every request.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

Client registration: three mechanisms, one preference order

A client needs a client ID before it can start. The specification names three ways to get one, and the order matters because it has shifted.

Mechanism Status
Client ID Metadata Documents Preferred. Servers and clients should support it. The client uses an HTTPS URL as its client_id; the authorization server fetches the metadata from that URL and validates it, including redirect_uris.
Pre-registration Fine when you control both sides and can register out of band.
Dynamic Client Registration (RFC 7591) Deprecated, retained for backwards compatibility with authorization servers that do not support Client ID Metadata Documents.

If you are building a client today, implement Client ID Metadata Documents and treat DCR as the fallback, not the default.

The resource parameter is not optional

This is the requirement most often skipped, and it is the one that prevents a stolen token from being replayed elsewhere.

Clients must implement Resource Indicators for OAuth 2.0 (RFC 8707). The resource parameter must appear in both the authorization request and the token request, must identify the MCP server the token is for, and must use that server's canonical URI. Clients must send it regardless of whether the authorization server supports it.

Valid canonical URIs look like https://mcp.example.com/mcp, https://mcp.example.com, or https://mcp.example.com:8443. Invalid ones omit the scheme or carry a fragment. Prefer the form without a trailing slash, and use the most specific URI you can when one host serves several MCP servers under different paths.

&resource=https%3A%2F%2Fmcp.example.com%2Fmcp

The server side of the same rule: an MCP server must validate that access tokens were issued specifically for it as the intended audience, must only accept tokens valid for its own resources, and must not accept or transit any other tokens. Clients must not send a server a token issued by anyone other than that server's authorization server.

Taken together, this is what closes the confused-deputy hole where a token minted for one service is handed to another that happily uses it.

Validate the issuer before you spend the code

Before redirecting the user-agent, the client must record the issuer value from the validated authorization-server metadata, stored alongside the PKCE verifier and state for that request. On the callback, it must validate the returned iss before transmitting the authorization code to any token endpoint.

authorization_response_iss_parameter_supported iss in response Client action
true present Compare to the recorded issuer, simple string comparison
true absent Reject the response
false or absent present Compare to the recorded issuer
false or absent absent Proceed

Two details decide whether this actually protects anything. The comparison must be a simple string comparison — no scheme or host case folding, no default-port elision, no trailing-slash or percent-encoding normalisation after URL-decoding. And it applies to error responses too: on a mismatch the client must not act on or display error, error_description, or error_uri.

Authorization servers should emit iss and advertise it. The specification signals that inclusion is expected to become a must in a future revision, so implement it now on both sides.

Scopes: ask for less, then ask again

The specification builds in a least-privilege path, and using it is mostly a matter of not ignoring what the server tells you.

Initial selection. In priority order: use the scope parameter from the WWW-Authenticate header of the 401 if present; otherwise use scopes_supported from the protected resource metadata; omit scope entirely if neither exists.

scopes_supported is meant to be the minimal set needed for basic functionality, not a catalogue of everything the server can do. Additional permissions are requested incrementally.

Clients must treat the scopes in a challenge as authoritative for the current operation, and must not assume any set relationship between them and scopes_supported — the challenge may be a subset, a superset, or neither.

Step-up at runtime. When a valid token lacks a permission, 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 parses the challenge, computes the union of its previously requested scopes and the challenged ones, re-authorizes with that set, and retries. The union step is what stops step-up from silently dropping permissions the client still needs for other operations — scope accumulation is the client's responsibility, not the server's.

Two obligations on the server side are worth stating plainly. Include all scopes needed for the current operation in a single challenge; drip-feeding one missing scope at a time forces a user through repeated authorization round trips for one action. And account for scope hierarchies, where a broader scope implies narrower ones, when deciding whether a token is sufficient.

Clients should implement retry limits and track upgrade attempts, so a permanently insufficient permission fails cleanly instead of looping.

Handling the token itself

  • Send it in the Authorization request header, on every request. There is no session that remembers it.
  • Never put an access token in a URI query string.
  • Keep refresh tokens confidential in transit and at rest. Clients that want them should include refresh_token in their grant_types and may add offline_access to the scope request when the authorization server advertises it — but must not assume refresh tokens will be issued.
  • Protected resources should not advertise offline_access in a WWW-Authenticate challenge or in scopes_supported, because a refresh token is a client convenience, not a resource requirement.

Invalid or expired tokens must receive 401. The three status codes carry distinct meanings and should not be used interchangeably:

Status Meaning
400 Malformed authorization request
401 Authorization required, or token invalid
403 Valid token, insufficient scope or permissions

Where enforcement actually has to live

The advertised list of tools, resources, or prompts may vary by the authorization presented on the request — returning only what the caller's granted scopes permit is explicitly allowed, because credentials are per-request input rather than connection state. The same lists must not vary per-connection or as a side effect of other requests.

But a filtered list is not access control. A caller who knows a tool name can call it. Authorization must be checked in the handler that performs the action, every time, against the token presented on that request.

The same applies to any state handle your tools return. Because MCP has no protocol-level session, servers that need continuity hand back explicit handles — a cart ID, a transaction ID. For an authenticated server, a handle is a name, not a capability: validate the caller's authorization against it on every call. For an unauthenticated server, where the handle necessarily is a bearer token, generate it with sufficient entropy and give it a bounded lifetime.

Troubleshooting

Repeated 401s after a successful sign-in. Usually an audience mismatch: the token was issued for a different resource, or the resource parameter did not match the server's canonical URI. Check the trailing slash and the path.

403 on one tool, 200 on the rest. An insufficient-scope challenge that the client is not acting on. Read the WWW-Authenticate header rather than the status code alone.

The callback never completes. A redirect URI that does not match what was registered, or a callback port the client did not actually bind. For Client ID Metadata Documents, confirm the redirect_uris in the document at your client_id URL.

Scopes shrink after a step-up. The client re-authorized with only the challenged scope instead of the union. This is the most common step-up bug and it presents as previously working tools starting to fail.

Everything works locally and fails when hosted. stdio took credentials from the environment; nothing in that path exercised OAuth. Treat the hosted deployment as a new integration.

An MCP server asks for a token issued by someone else. Stop. A server must not accept or transit tokens other than its own. Treat the request as a finding, and vet the server before going further.

Before you connect a work account

  • The server publishes protected resource metadata and challenges with a scope you can read.
  • The scopes requested match what the server's documentation says it does.
  • Write, delete, send, and publish permissions are absent unless you need them today.
  • The token is bound to this server by a resource parameter.
  • You know which account the grant is against, and you can revoke it at the provider.
  • You have tested a read-only operation before granting anything that changes state.

Revocation is the step people skip. Removing a server from a client stops that client from calling it; it does not necessarily end a grant the provider still honours. Revoke at the source.

Next step: Review an MCP server's authentication and permissions on LLM Mart before connecting a work account, and check the scopes it asks for against the ones it actually needs.

Sources

0 0 0 0 Sign in to react

Comments (0)

Sign in to join the conversation.

No comments yet.