Home » MCP Servers » Secure with OAuth

How to Secure an MCP Server with OAuth 2.1

OAuth 2.1 is the MCP specification's recommended authentication mechanism for production HTTP servers. It provides token-based access control with scopes, refresh flows, and multi-user support. You register your server with an OAuth provider, validate bearer tokens on each request, and use token scopes to control which tools each user can access.

Before You Start

You need an MCP server running with HTTP transport and an OAuth 2.1 provider. Major providers include Auth0, Okta, Keycloak, and cloud-native options like AWS Cognito and Google Identity Platform. If you already have an identity provider in your organization, use that. If not, Auth0 and Keycloak both have free tiers suitable for development and small deployments.

This guide covers the server-side implementation. MCP clients that support OAuth (including Claude Code and Cursor) handle the client-side flow automatically when they detect that a server requires OAuth authentication.

Step-by-Step Implementation

Step 1: Choose an OAuth provider.
Your OAuth provider is the authorization server that issues tokens. It handles user authentication, consent, and token management. You configure your MCP server to trust tokens from this provider and validate them on each request.

Key requirements for your provider: it must support OAuth 2.1 (specifically the authorization code flow with PKCE), it must issue JWTs (JSON Web Tokens) as access tokens (so your server can validate them locally without calling the provider on every request), and it must expose a JWKS (JSON Web Key Set) endpoint for token signature verification.

Step 2: Register your MCP server as a resource.
In your OAuth provider's dashboard, register your MCP server as an API or resource server. Define the scopes that represent different levels of access. Each scope maps to a set of tools that the token holder is allowed to invoke.
Scopes: mcp:read - Can invoke read-only tools (recall, search, status) mcp:write - Can invoke write tools (store, update, forget) mcp:admin - Can invoke administrative tools (reflect, configure)

Create a client application registration for each MCP client that will connect to your server. The client registration includes a client ID, a redirect URI for the OAuth callback, and the allowed scopes. The redirect URI depends on the MCP client implementation; check the client's documentation for the expected callback URL.

Step 3: Implement token validation.
Add middleware to your HTTP server that intercepts every MCP request, extracts the bearer token from the Authorization header, validates the token's signature using the provider's JWKS, checks the token's expiration, and extracts the user identity and scopes. Reject requests with missing, invalid, or expired tokens with a 401 response.
import jwt import requests from functools import lru_cache OAUTH_ISSUER = "https://your-provider.auth0.com/" AUDIENCE = "https://mcp.yourserver.com" @lru_cache(maxsize=1) def get_jwks(): url = f"{OAUTH_ISSUER}.well-known/jwks.json" return requests.get(url).json() def validate_token(auth_header): if not auth_header or not auth_header.startswith("Bearer "): return None token = auth_header[7:] jwks = get_jwks() header = jwt.get_unverified_header(token) key = None for k in jwks["keys"]: if k["kid"] == header["kid"]: key = jwt.algorithms.RSAAlgorithm.from_jwk(k) break if not key: return None try: payload = jwt.decode( token, key, algorithms=["RS256"], audience=AUDIENCE, issuer=OAUTH_ISSUER ) return payload except jwt.InvalidTokenError: return None
Step 4: Map scopes to tool access.
After validating the token, check which scopes it contains before allowing a tool invocation. Each tool handler should verify that the calling user has the required scope. Return a 403 response if the token is valid but lacks the required scope for the requested tool.
TOOL_SCOPES = { "recall": "mcp:read", "search": "mcp:read", "status": "mcp:read", "store": "mcp:write", "update": "mcp:write", "forget": "mcp:write", "reflect": "mcp:admin", } def check_scope(token_payload, tool_name): required = TOOL_SCOPES.get(tool_name, "mcp:admin") granted = token_payload.get("scope", "").split() return required in granted
Step 5: Handle token refresh.
Access tokens expire, typically after 15 minutes to an hour. When a client receives a 401 response to a previously working token, it should use the refresh token to obtain a new access token without requiring the user to re-authenticate. MCP clients that support OAuth handle this automatically, but your server needs to return consistent 401 responses so the client knows when to refresh.

Make sure your 401 responses include a WWW-Authenticate header that tells the client what went wrong. This helps the client distinguish between an expired token (refresh and retry) and an invalid token (re-authenticate from scratch).

WWW-Authenticate: Bearer realm="mcp", error="invalid_token", error_description="Token has expired"
Step 6: Test the full flow.
Walk through the complete authentication cycle: obtain an authorization code, exchange it for tokens, make an MCP request with the access token, verify scope enforcement, let the token expire, refresh it, and verify the refreshed token works. Use curl or a simple HTTP client to test each step independently before testing through an MCP client.

API Keys vs OAuth

For single-user or small-team deployments where simplicity matters more than enterprise features, API keys are a valid alternative to OAuth. The tradeoff is clear: API keys are simpler to implement (just compare strings) but lack token expiration, per-user scoping, and audit trails that OAuth provides natively.

Use API keys when: you have a small number of known users, you manage key rotation manually, and you do not need fine-grained access control. Use OAuth when: you have many users, you need automatic token lifecycle management, you need different permission levels, or your organization requires audit compliance.

Multi-Tenant Considerations

For servers that serve multiple organizations, each with their own data and access policies, the token's claims should include a tenant identifier. Your tool handlers use this identifier to scope all data access to the correct tenant. This prevents one organization's users from accessing another organization's data through the same MCP server.

Adaptive Recall handles multi-tenancy through API keys that are scoped to individual accounts. Each key maps to an isolated memory store, entity graph, and configuration set. The MCP tools automatically scope all operations to the key's account without any additional configuration from the user.

Skip the auth implementation. Adaptive Recall handles authentication, rate limiting, and multi-tenant isolation out of the box.

Get Started Free