top of page
Search

Secure your MCP Servers using OAuth 2.1 - Part 3

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:

  1. What you need to create in Auth0

  2. FastMCP server code using Auth0Provider

  3. Client code to connect (with OAuth flows handled for you)

  4. How the whole flow works end-to-end

1. Auth0 setup: what you need to create

You’ll do two main things in Auth0:

  1. Create / pick an API (resource server) → defines your MCP audience + scopes

  2. Create an Application → gives you client_id / client_secret & callback URL for the OAuth flow

We’ll assume:

1.1 Create / configure the API (resource server)

In Auth0, APIs are “resource servers” that issue access tokens for a specific audience. (Auth0)

  1. Go to Auth0 Dashboard → Applications → APIs → Create API. (FastMCP)

  2. 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)

  3. Click Create.

  4. 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”

  5. Save your changes.

You now have:

1.2 Create an Auth0 Application for MCP clients

This app is what the browser-based OAuth flow will use.

  1. In Auth0 Dashboard, go to Applications → Applications → Create Application. (FastMCP)

  2. 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)

  3. Click Create.

  4. In the Settings → Application URIs section, configure:

    For local dev:

    For prod:

  5. 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:

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:

  1. Client calls http://localhost:8000/mcp → gets 401 with resource_metadata in WWW-Authenticate. (FastMCP)

  2. Client fetches PRM → sees Auth0 as the authorization server.

  3. Client starts an OAuth 2.1 Authorization Code + PKCE flow in the browser.

  4. After login/consent, Auth0 redirects back to FastMCP’s callback (/auth/callback).

  5. 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:

  1. Auth0 side

  2. FastMCP server

  3. 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:

    • Receives access tokens and uses them in Authorization: Bearer <token> for MCP calls.

  4. Inside tools

    • get_access_token() exposes the validated token:

    • 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


Contact Us

Tel: 201-682-8623 / Email: support@gradientorbit.com

FAQ's

© 2025 by Gradient Web Designers.

Our Location

bottom of page