The moment an MCP server leaves your laptop and answers requests over HTTP, an open endpoint that exposes tools is a liability. The fix the spec prescribes is specific: your server becomes an OAuth 2.1 resource server, it validates a bearer token on every request, and it publishes a small metadata document that tells clients where to get a token. It does not issue tokens itself.
The one-line version: verify the incoming Authorization: Bearer token against your identity provider, reject anything whose audience is not your server, and serve /.well-known/oauth-protected-resource so clients can discover the authorization server on their own.
Key Takeaways
- The short answer: make the MCP server an OAuth 2.1 resource server that verifies bearer tokens and serves protected resource metadata; a separate authorization server issues the tokens.
- Authorization applies to HTTP-based transports only. A stdio server takes credentials from its environment and should not implement this flow.
- Your server MUST validate that each token's audience is your server. Accepting a token minted for another service is the single most common MCP auth vulnerability.
- Clients discover your authorization server through RFC 9728 protected resource metadata, returned via a 401
WWW-Authenticateheader or a well-known URL. - As of the 2025-11-25 spec revision, Client ID Metadata Documents are the preferred client registration path and Dynamic Client Registration is now optional (MAY), kept for backwards compatibility.
What You Need
- A working remote MCP server on an HTTP transport (streamable HTTP or SSE). If you are still on stdio, move to HTTP first, because the OAuth flow does not apply to stdio.
- An OAuth 2.1 authorization server you control or trust: Auth0, Keycloak, Okta, Microsoft Entra ID, WorkOS, or any provider that publishes JWKS and authorization server metadata.
- Python 3.10 or newer with
fastmcpinstalled (pip install fastmcp), or the equivalent auth middleware for the SDK you use. - Three values from your provider: the issuer URL, the JWKS URI, and an audience (resource) identifier that equals your server's canonical URI.
Add OAuth 2.1 to Your MCP Server, Step by Step
Work top to bottom. After step 5 you have a server that rejects unauthenticated calls and advertises how to authenticate.
Confirm you are on an HTTP transport. The MCP authorization specification states that stdio implementations SHOULD NOT follow this flow and instead read credentials from the environment. If you need to move first, see the guide on serving MCP over streamable HTTP.
Register your server as a resource in your identity provider. Create an API or resource entry whose identifier (audience) is the canonical URI of your MCP endpoint, for example
https://mcp.example.com/mcp. Note the issuer and JWKS URI the provider gives you.Add a token verifier. Point it at your provider's JWKS and pin the issuer and audience. The audience MUST match the resource identifier from step 2.
from fastmcp.server.auth.providers.jwt import JWTVerifier verifier = JWTVerifier( jwks_uri="https://auth.example.com/.well-known/jwks.json", issuer="https://auth.example.com", audience="https://mcp.example.com/mcp", required_scopes=["files:read"], )Wrap the verifier in a remote auth provider and attach it to the server. This is what turns your process into a resource server and auto-publishes the protected resource metadata endpoint.
from fastmcp import FastMCP from fastmcp.server.auth import RemoteAuthProvider from pydantic import AnyHttpUrl auth = RemoteAuthProvider( token_verifier=verifier, authorization_servers=[AnyHttpUrl("https://auth.example.com")], base_url="https://mcp.example.com", ) mcp = FastMCP(name="Files MCP", auth=auth)The FastMCP remote OAuth documentation confirms that
RemoteAuthProviderexposes/.well-known/oauth-protected-resourcefor you and validates bearer tokens on each request.Verify the challenge and the metadata with curl. An unauthenticated call must return 401 with a
WWW-Authenticateheader, and the metadata document must list your authorization server.$ curl -i https://mcp.example.com/mcp HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read" $ curl https://mcp.example.com/.well-known/oauth-protected-resource { "resource": "https://mcp.example.com/mcp", "authorization_servers": ["https://auth.example.com"], "scopes_supported": ["files:read", "files:write"], "bearer_methods_supported": ["header"] }Connect a client and let it run the flow. A spec-compliant client reads the 401, fetches the metadata, discovers the authorization server, runs the OAuth 2.1 code flow with PKCE, and retries with a token. Confirm a call with a valid token succeeds and a tampered token returns 401.
Why the Server Verifies but Never Issues Tokens
The 2025-06-18 revision split the roles cleanly, and the current 2025-11-25 revision keeps them split. Your MCP server is the resource server: it consumes tokens and returns data. The authorization server is a separate concern that authenticates the user and mints tokens. Keeping these apart is what lets you reuse an identity provider you already run instead of building an OAuth server from scratch.
Discovery is the glue. Because a general-purpose client has no prior knowledge of your server, it learns where to authenticate at runtime. Your server MUST implement RFC 9728 Protected Resource Metadata, and the authorization_servers field in that document MUST name at least one authorization server.
How Should Clients Register With Your Authorization Server?
The 2025-11-25 revision reordered the registration options. Client ID Metadata Documents, where a client uses an HTTPS URL as its client_id, are now the preferred path for the common case of a client and server with no prior relationship. Dynamic Client Registration dropped from SHOULD to MAY and is retained mainly for backwards compatibility.
| Mechanism | Best fit | Spec status (2025-11-25) |
|---|---|---|
| Client ID Metadata Documents | Client and server have no prior relationship (most common) | SHOULD support; preferred |
| Pre-registration | An existing relationship or static, hardcoded credentials | SHOULD support |
| Dynamic Client Registration (RFC 7591) | Backwards compatibility with earlier clients | MAY support |
What Does Audience Validation Actually Prevent?
Clients must implement Resource Indicators from RFC 8707, sending a resource parameter in both the authorization and token requests that names your server's canonical URI. Your server then validates that the token's audience claim matches. Skip this and your server will happily accept a token that a user granted to a different application, which breaks the OAuth trust boundary. The spec is blunt about the related trap: never forward a client's token to an upstream API. If your server calls another API, it acts as an OAuth client there and obtains its own separate token.
Troubleshooting
Valid token still returns 401. Almost always an audience mismatch. The token's
audclaim, theresourceparameter the client sent, and theaudienceyou configured on the verifier must all equal the same canonical server URI. Decode the token and compare the three.Client cannot find the authorization server. The protected resource metadata is missing or the 401 lacks a
WWW-Authenticateheader. Curl/.well-known/oauth-protected-resourceand confirm it returns JSON with a populatedauthorization_serversarray.403 with
insufficient_scope. The token is valid but lacks a required scope. Return a 403 whoseWWW-Authenticateheader carrieserror="insufficient_scope"and ascopeparameter listing what the operation needs, so the client can run a step-up authorization.Client refuses to proceed, citing PKCE. Your authorization server metadata omits
code_challenge_methods_supported. Spec-compliant clients treat a missing value as no PKCE support and abort. Enable PKCE on the authorization server and advertiseS256.Transport mismatch. If you bolted auth onto a stdio server, nothing enforces it. Move to an HTTP transport before expecting the flow to work.
What to Do Next
- Exercise the full handshake with the MCP Inspector, which walks the 401, discovery, and token steps and shows exactly where a flow breaks.
- Review the MCP security best practices for token passthrough, confused-deputy, and scope-minimization guidance before going to production.
- If you are building the server itself, start from the FastMCP server guide and layer this auth provider on top.