Secure your MCP Servers using OAuth 2.1 - Part 3
- amareshpat
- Nov 11
- 5 min read
Here’s how to wire your MCP server to Auth0 using FastMCP in a way that’s actually deployable, not just “hello world”.
I’ll split it into:
What you need to create in Auth0
FastMCP server code using Auth0Provider
Client code to connect (with OAuth flows handled for you)
How the whole flow works end-to-end
1. Auth0 setup: what you need to create
You’ll do two main things in Auth0:
Create / pick an API (resource server) → defines your MCP audience + scopes
Create an Application → gives you client_id / client_secret & callback URL for the OAuth flow
We’ll assume:
Your public MCP URL: https://mcp.example.com
Your Auth0 domain: your-tenant.us.auth0.com (or a custom domain if you’ve set one up)
Your API audience: https://mcp.example.com (matches your resource identifier)
1.1 Create / configure the API (resource server)
In Auth0, APIs are “resource servers” that issue access tokens for a specific audience. (Auth0)
Go to Auth0 Dashboard → Applications → APIs → Create API. (FastMCP)
Fill in:
Name: MCP Weather API (anything human-readable)
Identifier: https://mcp.example.com
This becomes your audience and will land in the aud claim of access tokens.
Signing Algorithm: RS256 (recommended)
Click Create.
In the API settings, add at least one scope (this is what you’ll enforce inside tools): (Auth0)
Example:
weather:read – “Read current weather data”
Save your changes.
You now have:
Audience: https://mcp.example.com
Scopes: weather:read (and more as you add them)
1.2 Create an Auth0 Application for MCP clients
This app is what the browser-based OAuth flow will use.
In Auth0 Dashboard, go to Applications → Applications → Create Application. (FastMCP)
Choose:
Name: MCP Weather Server (this is what users will see on the Auth0 login page)
Application Type: Single Page Web Applications (works well with PKCE-based flows)
Click Create.
In the Settings → Application URIs section, configure:
For local dev:
Allowed Callback URLs:http://localhost:8000/auth/callback(this must match base_url + redirect_path that we’ll set in FastMCP; default redirect path is /auth/callback) (FastMCP)
For prod:
Allowed Callback URLs:https://mcp.example.com/auth/callback
Scroll up and note:
Client ID – public identifier (e.g. tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB)
Client Secret – keep this in env / secret manager, never in git
We’ll use these values in the FastMCP server config.
1.3 Know your Auth0 endpoints
Auth0 gives you standard OIDC/OAuth metadata:
OIDC discovery / config URL (for config_url):https://YOUR_AUTH0_DOMAIN/.well-known/openid-configuration (Auth0)
JWKS endpoint (FastMCP will read this via OIDC):https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json (Auth0)
FastMCP’s Auth0Provider just needs the discovery URL; it will follow it to find the JWKS, token endpoint, etc. (FastMCP)
2. FastMCP server using Auth0Provider
FastMCP already ships a dedicated Auth0 integration: fastmcp.server.auth.providers.auth0.Auth0Provider. It wraps the OIDC Proxy pattern and exposes:
OAuth 2.1-compliant Protected Resource Metadata (/.well-known/oauth-protected-resource)
WWW-Authenticate challenges
Token validation and claim parsing
Persistent client/token storage (if configured) (FastMCP)
2.1 Minimal server.py
# server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.dependencies import get_access_token, AccessToken
# ── Auth0 configuration ──────────────────────────────────────────────
AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN", "your-tenant.us.auth0.com")
AUTH0_CONFIG_URL = f"https://{AUTH0_DOMAIN}/.well-known/openid-configuration"
AUTH0_CLIENT_ID = os.environ["AUTH0_CLIENT_ID"]
AUTH0_CLIENT_SECRET = os.environ["AUTH0_CLIENT_SECRET"]
AUTH0_AUDIENCE = os.environ.get("AUTH0_AUDIENCE", "https://mcp.example.com")
# Public MCP URL (for local dev)
BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000")
auth_provider = Auth0Provider(
config_url=AUTH0_CONFIG_URL,
client_id=AUTH0_CLIENT_ID,
client_secret=AUTH0_CLIENT_SECRET,
audience=AUTH0_AUDIENCE,
base_url=BASE_URL,
# redirect_path="/auth/callback", # default; must match Auth0 Allowed Callback URLs
)
# ── FastMCP server ───────────────────────────────────────────────────
mcp = FastMCP(name="Auth0 Weather MCP", auth=auth_provider)
@mcp.tool
async def current_weather(city: str) -> dict:
"""
Get current weather for a city.
Requires Auth0 access token with `weather:read` scope.
"""
token: AccessToken | None = get_access_token()
if token is None:
# Shouldn't happen under HTTP, but good defensive check
raise PermissionError("Not authenticated")
# Auth0 usually sends scopes as a space-separated string in the "scope" claim.
scope_raw = token.claims.get("scope", "")
if isinstance(scope_raw, str):
scopes = scope_raw.split()
else:
scopes = list(scope_raw or [])
required_scope = "weather:read"
if required_scope not in scopes:
raise PermissionError(f"Missing required scope: {required_scope}")
user_id = token.claims.get("sub", "unknown")
# TODO: replace with real weather provider integration
return {
"city": city,
"conditions": "sunny",
"temperature_c": 23.5,
"requested_by": user_id,
"scopes": scopes,
"issuer": token.claims.get("iss"),
"audience": token.claims.get("aud"),
}
if __name__ == "__main__":
# Expose MCP over HTTP so OAuth flows can happen
mcp.run(transport="http", host="0.0.0.0", port=8000)
What this does (under the hood):
Uses Auth0’s OIDC discovery (config_url) to find:
token endpoint
jwks_uri
authorization endpoint, etc. (Auth0)
Exposes a Protected Resource Metadata endpoint (/.well-known/oauth-protected-resource) describing:
resource (your MCP endpoint)
authorization_servers (Auth0 domain)
Returns 401 + WWW-Authenticate: Bearer … resource_metadata=… when a client connects without a token.
Validates incoming Auth0 tokens (RS256 signature via JWKS, issuer, audience, expiry). (FastMCP)
Provides get_access_token() inside tools with claims + scopes already parsed.
2.2 Optional: environment-based auth config
The docs also show that you can configure this entirely via env vars and let FastMCP auto-wire the provider: (FastMCP)
# .env
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider
FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://your-tenant.us.auth0.com/.well-known/openid-configuration
FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID=tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB
FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET=YOUR_SECRET_HERE
FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE=https://mcp.example.com
FASTMCP_SERVER_AUTH_AUTH0_BASE_URL=http://localhost:8000
FASTMCP_SERVER_AUTH_AUTH0_REQUIRED_SCOPES=openid weather:read
Then server code shrinks to:
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_access_token, AccessToken
mcp = FastMCP(name="Auth0 Weather MCP")
@mcp.tool
async def current_weather(city: str) -> dict:
token: AccessToken | None = get_access_token()
...
FastMCP reads FASTMCP_SERVER_AUTH_* and instantiates Auth0Provider automatically. (FastMCP)
3. Client: connecting to the Auth0-secured MCP server
You have two main options:
FastMCP Python client (auth="oauth")
Any MCP-aware client (ChatGPT, Claude, etc.) that follows PRM + OAuth 2.1
3.1 Python test client (auth="oauth")
FastMCP’s Auth0 integration guide shows a very compact client; the server’s PRM + Auth0Provider do most of the work. (FastMCP)
# client.py
import asyncio
from fastmcp import Client
async def main():
# FastMCP client auto-detects OAuth via the server’s PRM
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
# First run will open Auth0 login in the browser
tools = await client.list_tools()
print("Tools:", [t.name for t in tools])
result = await client.call_tool(
name="current_weather",
arguments={"city": "San Francisco"},
)
print("Weather:", result.content[0].as_json())
if __name__ == "__main__":
asyncio.run(main())
On first run:
Client calls http://localhost:8000/mcp → gets 401 with resource_metadata in WWW-Authenticate. (FastMCP)
Client fetches PRM → sees Auth0 as the authorization server.
Client starts an OAuth 2.1 Authorization Code + PKCE flow in the browser.
After login/consent, Auth0 redirects back to FastMCP’s callback (/auth/callback).
FastMCP completes the token exchange, stores tokens, and injects them into MCP calls.
You don’t need to manually deal with redirect URIs in the client – Auth0Provider + auth="oauth" coordinate that.
4. Putting it all together: end-to-end flow
From the system’s point of view:
Auth0 side
You created an API (https://mcp.example.com) with scopes (weather:read). (FastMCP)
You created an Application (SPA) with Allowed Callback URLs set to https://mcp.example.com/auth/callback (and/or http://localhost:8000/auth/callback for dev). (FastMCP)
FastMCP server
Uses Auth0Provider with:
config_url = https://YOUR_AUTH0_DOMAIN/.well-known/openid-configuration
client_id / client_secret from the Auth0 Application
audience = https://mcp.example.com (Auth0 API identifier)
base_url = https://mcp.example.com (or http://localhost:8000 for dev) (FastMCP)
Exposes MCP at /mcp and OAuth callback at /auth/callback.
MCP client (FastMCP Client, ChatGPT, Claude, etc.)
Connects to https://mcp.example.com/mcp.
Learns from PRM that Auth0 is the authorization server.
Runs OAuth 2.1 Authorization Code + PKCE against Auth0 with:
audience=https://mcp.example.com
scope=openid weather:read
Receives access tokens and uses them in Authorization: Bearer <token> for MCP calls.
Inside tools
get_access_token() exposes the validated token:
token.claims["sub"] → user ID
token.claims["scope"] → "openid weather:read"
You enforce authorization based on scopes, tenant IDs, roles, etc.
If you’d like, next iteration we can:
Expand this into a multi-tenant Auth0 setup (scoping by org/tenant IDs in custom claims).
Add RBAC using Auth0 roles → map them to tool-level permissions in FastMCP.
Or integrate a real backend API (e.g., weather / Pure Storage REST API) behind your MCP tools, still gated by Auth0 scopes.


Comments