MCP.so
Sign In
Back to blog

MCP Authentication: How OAuth 2.1 Works in MCP (2026 Guide)

Oct 15, 2026MCP.so TeamMCP.so Team

MCP authentication explained: OAuth 2.1, protected resource metadata, PKCE, client ID metadata, resource indicators, stdio env credentials and API keys.

How MCP Authentication Works

MCP authentication for remote (HTTP) servers uses OAuth 2.1: the MCP server is an OAuth resource server, the MCP client is an OAuth client, and a separate authorization server issues access tokens that the client sends as Authorization: Bearer on every request. Local stdio servers do not use this flow; per the spec they take credentials from the environment, usually API keys passed as env vars. This guide follows the current spec revision, 2026-07-28, and shows what changed, what each piece does, and how to implement it with the official SDKs.

stdio vs HTTP: Which Auth Model Applies?

The authorization spec makes authorization optional, then splits by transport:

  • HTTP-based transports SHOULD conform to the MCP authorization spec (OAuth 2.1).
  • stdio transports SHOULD NOT follow it, and instead retrieve credentials from the environment.
  • Other transports MUST follow established security practices for their protocol.
Local stdio serverRemote HTTP server
Who runs itYour machine, as a child process of the clientA hosted service
Typical credentialAPI key or token in an env varOAuth 2.1 access token
Who logs inYou, when you create the keyYou, in a browser, during the OAuth flow
RevocationRotate the key at the providerRevoke the grant or let short-lived tokens expire
Spec flowNone, read from environmentProtected resource metadata, discovery, PKCE

The OAuth 2.1 Flow, Step by Step

1. The server answers 401 with a pointer

The client calls the MCP endpoint without a token. The server responds 401 with a WWW-Authenticate header naming its Protected Resource Metadata document (RFC 9728) and, optionally, the scopes this request needs:

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

2. The client reads protected resource metadata

MCP servers MUST implement RFC 9728, and the document MUST list at least one entry in authorization_servers:

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

If the header has no resource_metadata, clients MUST fall back to well-known URIs in this order: the path-specific URI (https://example.com/.well-known/oauth-protected-resource/public/mcp for an endpoint at /public/mcp), then the root /.well-known/oauth-protected-resource.

3. The client discovers the authorization server

Authorization servers MUST offer RFC 8414 metadata or OpenID Connect Discovery, and clients MUST support both. For an issuer without a path, clients try /.well-known/oauth-authorization-server, then /.well-known/openid-configuration. For an issuer with a path such as https://auth.example.com/tenant1, they try the OAuth path-insertion URL, then the OIDC path-insertion URL, then the OIDC path-appended URL. The issuer in the returned document MUST match the issuer used to build the URL, or the client MUST discard it.

4. The client gets a client ID

Before the flow starts, the client needs a client_id. The client registration page defines three mechanisms and a priority order:

PriorityMechanismWhen it applies
1Pre-registrationThe client already has a client ID for this authorization server
2Client ID Metadata Documents (CIMD)The authorization server advertises client_id_metadata_document_supported
3Dynamic Client Registration (RFC 7591)Fallback when the server exposes a registration_endpoint
4Ask the userNothing else is available

The big change in recent revisions: Dynamic Client Registration is deprecated. New implementations should use CIMD, where the client_id is an HTTPS URL that points to a JSON document the authorization server fetches:

{
  "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 URL MUST use https with a path, the document MUST include client_id, client_name and redirect_uris, and client_id MUST match the URL exactly. CIMD client IDs are portable across authorization servers; pre-registered and DCR credentials MUST be bound to the issuer that created them.

5. Authorization code flow with PKCE and resource indicators

The client opens the browser to the authorization endpoint. Three requirements matter most:

  • PKCE is mandatory. Clients MUST use S256 when technically capable, and MUST check code_challenge_methods_supported in the server metadata; if it is absent, the client MUST refuse to proceed (security considerations).
  • The resource parameter is mandatory. Clients MUST send RFC 8707 resource in both the authorization and token requests, set to the MCP server's canonical URI (for example https://mcp.example.com/mcp, no fragment, preferably no trailing slash), even if the authorization server ignores it.
  • Validate the issuer. Before redirecting, the client records the expected issuer; when the code comes back, it compares the iss parameter (RFC 9207) and rejects mismatches. This stops mix-up attacks between authorization servers.

Scope selection: use the scope from the 401 challenge if present; otherwise request what scopes_supported lists.

6. Calling the server with the token

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

The token MUST go in the Authorization header on every request and MUST NOT appear in the query string. The server MUST validate it and confirm it was issued for this server as audience; invalid or expired tokens get 401.

7. Step-up when a tool needs more

If a token lacks a scope, the server SHOULD return 403 with error="insufficient_scope" and the scopes this operation needs. The client re-authorizes with the union of previously requested and newly challenged scopes, and retries a limited number of times.

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"

Rules for Server Authors

  • Never pass tokens through. MCP servers MUST only accept tokens issued for themselves and MUST NOT forward the client's token to upstream APIs. If you call GitHub or Slack, get a separate upstream token. See MCP security best practices for why.
  • Use a real authorization server. The official authorization tutorial advises against writing token validation yourself; it uses Keycloak and validates the audience via introspection.
  • Keep tokens short-lived. Authorization servers SHOULD issue short-lived access tokens and MUST rotate refresh tokens for public clients.
  • Serve everything over HTTPS. Authorization endpoints MUST be HTTPS, and redirect URIs MUST be localhost or HTTPS.
  • Do not log credentials. Scrub Authorization headers, codes and tokens from logs.

Implementation Pointers in the Official SDKs

TypeScript. The official tutorial's server uses helpers from @modelcontextprotocol/sdk: mcpAuthMetadataRouter publishes the protected resource metadata, requireBearerAuth enforces tokens with your verifier, and getOAuthProtectedResourceMetadataUrl builds the URL for the WWW-Authenticate challenge:

import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import {
  getOAuthProtectedResourceMetadataUrl,
  mcpAuthMetadataRouter,
} from '@modelcontextprotocol/sdk/server/auth/router.js';

app.use(
  mcpAuthMetadataRouter({
    oauthMetadata,
    resourceServerUrl: mcpServerUrl,
    scopesSupported: ['mcp:tools'],
    resourceName: 'MCP Demo Server',
  })
);

const authMiddleware = requireBearerAuth({
  verifier: tokenVerifier,
  requiredScopes: [],
  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
});

Python. In the Python SDK, the MCPServer class takes a token_verifier (a subclass of TokenVerifier) and AuthSettings. It publishes the metadata document and answers unauthenticated requests with the right 401:

from pydantic import AnyHttpUrl
from mcp.server import MCPServer
from mcp.server.auth.settings import AuthSettings

app = MCPServer(
    name="MCP Resource Server",
    token_verifier=token_verifier,
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com/realms/master"),
        required_scopes=["mcp:tools"],
        resource_server_url=AnyHttpUrl("https://mcp.example.com/mcp"),
    ),
)

Both SDKs also ship checkResourceAllowed / check_resource_allowed helpers for comparing a token's resource with your server URL. To watch the whole handshake, connect with the MCP Inspector.

API Keys and Environment Credentials

Most servers in the MCP directory still authenticate with API keys. That is fine, and it is exactly what the spec describes for stdio.

stdio with an env var (Claude Code example from the official docs):

claude mcp add --env AIRTABLE_API_KEY=YOUR_KEY --transport stdio airtable \
  -- npx -y airtable-mcp-server

Remote HTTP server that accepts a bearer key:

claude mcp add --transport http secure-api https://api.example.com/mcp \
  --header "Authorization: Bearer your-token"

Remote HTTP server with OAuth: add it without credentials and log in from the client. For example, claude mcp add --transport http sentry https://mcp.sentry.dev/mcp, then run /mcp inside Claude Code and finish in the browser. The Sentry MCP Server, Notion MCP Server and GitHub MCP Server all offer hosted endpoints. More client-specific steps are in our Claude Code MCP guide.

To keep keys out of committed config, reference env vars in .mcp.json:

{
  "mcpServers": {
    "api-server": {
      "type": "http",
      "url": "https://api.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${API_KEY}"
      }
    }
  }
}

Static keys work, but they are long-lived and usually broad. OAuth gives you per-user consent, scoped and expiring tokens, and revocation without rotating a shared secret.

Troubleshooting MCP Authentication

SymptomLikely causeFix
Client never opens a browserServer returns 401 without resource_metadata and no well-known document existsServe /.well-known/oauth-protected-resource or add the header
"PKCE not supported" or the client refuses to continueAuthorization server metadata lacks code_challenge_methods_supportedEnable PKCE S256 and publish the field
Client registration failsNo CIMD support, no registration_endpoint, or OIDC rejects the redirect URIEnable CIMD, pre-register the client, or set application_type to native for local clients
Token accepted by the AS but server returns 401Audience mismatchMake the token audience match the canonical server URI from the resource parameter
Repeated 403 loopsServer challenges one missing scope at a timeReturn all scopes needed for the operation in one challenge

Next Steps

Pick a hosted server that supports OAuth from the MCP server directory, connect it with your client, and inspect the flow. If you run your own server, read MCP security best practices before you ship.

Frequently asked questions

Does MCP use OAuth?

Yes. For remote servers on HTTP transports, the MCP authorization spec is based on OAuth 2.1, with protected resource metadata, PKCE and resource indicators. Authorization is optional, and local stdio servers are expected to read credentials from the environment instead.

How do I authenticate a local MCP server?

Local stdio servers usually take an API key or token from an environment variable that your client passes when it launches the server. The spec says stdio implementations should not use the OAuth flow and should retrieve credentials from the environment.

Is dynamic client registration required for MCP?

No. In the 2026-07-28 revision, dynamic client registration is deprecated and kept for backwards compatibility. Clients should prefer pre-registered client IDs, then Client ID Metadata Documents, and fall back to dynamic registration only when the authorization server supports nothing else.

Can I use an API key with a remote MCP server?

Many hosted servers accept an API key sent in an Authorization header, and clients such as Claude Code let you set that header in the server config. It works, but OAuth gives you per-user consent, scoped expiring tokens and easier revocation.