Secure your MCP server with OAuth 2.1 in 60 minutes
When you need auth for your MCP server and when you don't. PKCE mandatory, .well-known discovery, Resource Indicators (RFC 8707). Three setup options: WorkOS AuthKit, Auth0 for MCP, or your own authorization server.
You built an MCP server, it first ran locally as stdio. Now you want to host it as a Streamable HTTP server so that other people or Cursor/Claude.ai/VSCode plugins can access it. At the latest there you need auth, otherwise anyone with a URL can read, write or call tools on your server.
The MCP spec requires OAuth 2.1 with PKCE as the auth standard. That is not optional. Here is how that works in practice and which of the three setup options fits your situation.
1. When you need auth and when you don't
First the clarification. Local MCP servers that run via stdio do not need OAuth. The MCP spec says explicitly: stdio implementations should read credentials from the environment, not go through OAuth flows. You pass API keys in via .env or the env block in the MCP config, done.
OAuth becomes mandatory as soon as your server is exposed over HTTP. Streamable HTTP, Server-Sent Events, classic request-response. As soon as data goes over the network and it is about protected resources, you need auth.
Third case: read-only demo server without sensitive data. In theory you can run without auth if your server only exposes public data (e.g. a weather MCP server). In practice though that causes problems because Claude.ai and other clients expect the auth handshake and get confused without a header. Even for public demos the minimal setup is worth it.
2. What the spec requires
The MCP spec 2025-06-18 requires five things if you implement OAuth.
First: OAuth 2.1, not OAuth 2.0. The difference is essential. 2.1 requires PKCE for all flows (not just public clients), drops the implicit grant completely, and requires exact redirect URI matching. Whoever implements 2.0 is already at the state from 5 years ago.
Second: PKCE with the S256 method. The client generates a random code_verifier, hashes it as code_challenge with SHA256, sends that in the authorization request, and on the token exchange it sends along the original code_verifier. Without that the MCP client does not even start.
Third: discovery via .well-known/oauth-protected-resource (RFC 9728). When a client accesses your MCP server for the first time, it sends a request without a token. You answer with HTTP 401 and a WWW-Authenticate header that points to /.well-known/oauth-protected-resource. There lies a JSON document that points to the authorization server.
Fourth: Resource Indicators (RFC 8707). The MCP client must send along a resource parameter in the authorization and token request that specifies the canonical URI of your MCP server. Example: &resource=https%3A%2F%2Fmcp.studiomeyer.io. With that the token is bound to your server, a stolen token cannot be misused for another MCP server.
Fifth: token audience validation. When a request comes in with a token, you have to check server-side that the token was actually issued for your canonical URI, not for another server. That prevents the "confused deputy" vulnerability.
3. Option A, WorkOS AuthKit
If you have no auth system of your own and want to go live fast, WorkOS is currently the simplest solution. AuthKit handles the complete OAuth authorization server side for you. Login page, user management, token issuance, PKCE, discovery endpoints, all done.
Setup roughly in 4 steps:
- Create a WorkOS account (workos.com), a new project for your MCP server, configure the AuthKit domain.
- In your MCP server set up a
/.well-known/oauth-protected-resourceendpoint that points tohttps://your-domain.authkit.app/.well-known/oauth-authorization-server. - In the server validate every request: read the
Authorization: Bearer ...token, validate it against the WorkOS JWKS endpoint (standard JWT verify library), check the audience claim against your MCP server URI, otherwise 401. - The login URL in your client (Claude.ai etc.) points to the WorkOS Universal Login page, from there the user comes back with a token.
Pricing is currently free for up to 1 million MAU with standard features, after that enterprise prices. For 99 percent of MCP servers this costs nothing.
Advantage: minimal code of your own, everything enterprise-ready (SAML, SCIM in case you need that later). Disadvantage: vendor lock-in. You depend on WorkOS availability. For production MCP servers in 2026 this is currently the de facto recommendation of the MCP community.
4. Option B, Auth0 for MCP
Auth0 has had a dedicated "Auth for MCP" product since May 2026. If you already use Auth0 for your app anyway, that is the natural choice. You get MCP-specific features like CIMD registration and on-behalf-of token exchange for AI agents on top.
Setup pattern similar to WorkOS:
- Set up an Auth0 tenant, a new application of type "Machine to Machine" or "Native" depending on the client.
- Configure the MCP specifics: Universal Login as the authorization server, PKCE mandatory, enable resource indicators support.
- In the MCP server JWT validation against the Auth0 JWKS endpoint, audience on your MCP URL.
- Make the discovery endpoints available in Auth0, stick on a local
/.well-known/oauth-protected-resourceendpoint.
Advantage over WorkOS: deeper enterprise features (anomaly detection, risk scoring), a larger pool of devs who already know Auth0, better SAML integration in case you sell into enterprise. Disadvantage: bigger pricing tiers, somewhat more complex configuration. Auth0 was bought by Okta in 2021, which has influenced the pricing model and velocity.
If you do not yet have an identity solution, WorkOS is faster to go live. If you already use Auth0, "Auth for MCP" goes more smoothly.
5. Option C, Cloudflare Workers OAuth Library
For self-hosters who do not want an external provider there is the Cloudflare OAuth library. It implements the complete provider side (DCR, PKCE, discovery) as a Cloudflare Worker. Standalone or combined with Cloudflare Access (their SSO product).
This is the variant that many current production MCP servers use internally, often as a layer under a provider like WorkOS or Auth0. As of May 2026 the library is open source, written in TypeScript, runs completely in the Worker environment.
Setup effort larger than A or B, but you have full control. Check beforehand: are you already hosting on Cloudflare anyway? Do you have bandwidth for auth operations as additional maintenance? If both answers are yes, this option is cheaper and more independent in the long run.
Plus: because this is an open-source library, you can apply the auth layer to multiple MCP servers at once without setting up a WorkOS or Auth0 account per server.
6. Discovery setup, the decisive step
No matter which option you take, the MCP client expects a clear discovery path. Sketch:
1. Client schickt MCP-Request ohne Token an https://mcp.example.com/mcp
2. Server antwortet 401 mit:
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
3. Client GETet https://mcp.example.com/.well-known/oauth-protected-resource
4. Server liefert JSON:
{
"resource": "https://mcp.example.com",
"authorization_servers": ["https://your-domain.authkit.app"]
}
5. Client GETet https://your-domain.authkit.app/.well-known/oauth-authorization-server
6. Authorization Server liefert Metadata mit Endpoints (authorize, token, jwks_uri).
That is the spec requirement. If one of these five steps is missing, the auto-discovery flow fails and your server is not usable by Claude.ai or Cursor.
You can also serve the resource_metadata endpoint statically. A simple HTML file on your web server with the JSON body, content type application/json, done.
7. Token validation in the server
In the MCP server you have to validate every request with a token. Pseudocode:
async function validateToken(req): Promise<TokenClaims> {
const auth = req.headers.get("authorization");
if (!auth || !auth.startsWith("Bearer ")) throw new Unauthorized();
const token = auth.slice(7);
const claims = await verifyJWT(token, JWKS_URI);
// Audience-Validation, RFC 8707
if (claims.aud !== "https://mcp.example.com") {
throw new Unauthorized("token audience mismatch");
}
// Expiry
if (claims.exp * 1000 < Date.now()) throw new Unauthorized("expired");
return claims;
}
The aud check is critical. Without it tokens for other services could be misused to talk to your MCP server. That is the most common implementation gap.
scope and sub (user ID) you read from the token claim if you need differentiated permissions or per-user logic. Standard MCP does not strictly use that, but you will need it as soon as you go multi-tenant.
8. Refresh token rotation, often overlooked
Public clients (so browser-based or mobile apps like Claude.ai) must rotate refresh tokens. Meaning: every time the client exchanges a refresh token for an access token, the authorization server returns a new refresh token and the old one is dead.
If you use WorkOS or Auth0, they do that automatically. If you go self-host, you have to implement it yourself. Non-rotating refresh tokens are forbidden in OAuth 2.1 because otherwise a stolen token can be used any number of times.
Check your provider: for self-host check the library docs, for a provider the default settings. With some providers token rotation has to be enabled explicitly.
9. The three pitfalls
Missing audience validation. You build yourself the OAuth flow, everything works, tokens get issued, the server accepts them. What you forget: checking whether the aud claim is your server URI. Consequence: someone gets a token from another service, sends it to your MCP server, you accept it. The spec calls that "confused deputy". Mandatory fix: aud validation always.
Resource parameter gets forgotten. The MCP client must send along the resource parameter in every authorization and token request. If the client does not do that (often with self-built clients), the auth server issues tokens without the correct audience claim. Workaround: document in your server docs that MCP clients must implement RFC 8707 resource indicators.
HTTPS requirement ignored. All authorization server endpoints must run over HTTPS, redirect URIs must be either localhost or HTTPS. If in dev you often test with HTTP setups and then bolt HTTPS onto production, you subtly break the trust chain. Test in dev with local self-signed certs or ngrok instead of with HTTP.
10. How you should start
If you do not yet have an MCP server in production: build it first as a stdio server, get feature complete, deploy locally. Only then think about HTTP hosting and thus about OAuth.
If you need HTTP hosting: pick WorkOS AuthKit, put the auth layer around it in 1 to 2 hours, ship. Optimize later when loads or special features come.
If you sell into enterprise: pick Auth0 for SAML/SCIM integration. More setup effort, but enterprise compliance covered.
If you are DIY and cost-conscious: Cloudflare Worker OAuth library plus Cloudflare Access. 2 to 4 days setup, after that you scale for almost nothing.
Also look at the playbook mcp-server-publishen once you have the server finished and want to distribute on MCP marketplaces, that is about discovery listings that are important beyond the OAuth setup.
Source
Specs verified via official MCP documentation and provider docs on 2026-05-07:
- modelcontextprotocol.io/specification/2025-06-18/basic/authorization (OAuth 2.1 requirements, PKCE mandatory, Resource Indicators, Discovery, token audience validation)
- workos.com/blog/best-mcp-server-authentication-providers (provider comparison May 2026, WorkOS AuthKit, Auth0 GA, Cloudflare library)
- prefect.io/resources/mcp-oauth (FastMCP OAuth implementation, token lifecycle)
Recipes consulted in the Academy:
- Phase 6 MCP Authorization Patterns
- Recipe 6.4 Token Validation
- Lesson L4-06 MCP-Discovery und Marketplaces