From c3d1176a69599b2080acfaeedf7a5be1b4bd9715 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Wed, 20 Aug 2025 16:22:03 -0400
Subject: [PATCH] Add WorkOS and Azure OAuth providers (#1550)
---
.pre-commit-config.yaml | 4 +-
docs/deployment/self-hosted.mdx | 2 +-
docs/docs.json | 95 ++++--
docs/integrations/anthropic.mdx | 2 +-
docs/integrations/{starlette.mdx => asgi.mdx} | 4 +-
docs/integrations/azure.mdx | 250 ++++++++++++++
docs/integrations/claude-desktop.mdx | 2 +-
docs/integrations/gemini.mdx | 2 +-
docs/integrations/github.mdx | 10 +-
docs/integrations/google.mdx | 10 +-
docs/integrations/openai.mdx | 2 +-
docs/integrations/workos-oauth.mdx | 240 +++++++++++++
.../fastmcp-prompts-prompt_manager.mdx | 14 +-
.../fastmcp-resources-resource_manager.mdx | 20 +-
.../fastmcp-server-auth-oauth_proxy.mdx | 319 ++++++++++++++++++
.../fastmcp-server-auth-providers-azure.mdx | 63 ++++
.../fastmcp-server-auth-providers-github.mdx | 72 ++++
.../fastmcp-server-auth-providers-google.mdx | 72 ++++
.../fastmcp-server-auth-providers-workos.mdx | 64 +++-
docs/python-sdk/fastmcp-server-server.mdx | 2 +-
.../python-sdk/fastmcp-tools-tool_manager.mdx | 22 +-
docs/python-sdk/fastmcp-utilities-tests.mdx | 10 +-
docs/servers/auth/oauth-proxy.mdx | 14 +-
examples/auth/azure_oauth/README.md | 51 +++
examples/auth/azure_oauth/client.py | 32 ++
examples/auth/azure_oauth/server.py | 39 +++
examples/auth/github_oauth/README.md | 2 +-
examples/auth/github_oauth/server.py | 2 +-
examples/auth/google_oauth/README.md | 2 +-
examples/auth/google_oauth/server.py | 2 +-
examples/auth/workos_oauth/README.md | 159 +++++++++
examples/auth/workos_oauth/client.py | 32 ++
examples/auth/workos_oauth/server.py | 37 ++
.../server/auth/{proxy.py => oauth_proxy.py} | 40 +--
src/fastmcp/server/auth/providers/azure.py | 250 ++++++++++++++
src/fastmcp/server/auth/providers/github.py | 6 +-
src/fastmcp/server/auth/providers/google.py | 13 +-
src/fastmcp/server/auth/providers/workos.py | 233 ++++++++++++-
src/fastmcp/server/server.py | 2 +-
.../auth/test_github_provider_integration.py | 4 +-
tests/server/auth/__init__.py | 0
tests/server/auth/providers/__init__.py | 0
tests/server/auth/providers/test_azure.py | 153 +++++++++
tests/server/auth/providers/test_github.py | 14 +-
tests/server/auth/providers/test_google.py | 105 ++++++
tests/server/auth/providers/test_workos.py | 141 ++++++++
tests/server/auth/test_oauth_proxy.py | 43 +--
47 files changed, 2488 insertions(+), 169 deletions(-)
rename docs/integrations/{starlette.mdx => asgi.mdx} (98%)
create mode 100644 docs/integrations/azure.mdx
create mode 100644 docs/integrations/workos-oauth.mdx
create mode 100644 docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
create mode 100644 docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
create mode 100644 docs/python-sdk/fastmcp-server-auth-providers-github.mdx
create mode 100644 docs/python-sdk/fastmcp-server-auth-providers-google.mdx
create mode 100644 examples/auth/azure_oauth/README.md
create mode 100644 examples/auth/azure_oauth/client.py
create mode 100644 examples/auth/azure_oauth/server.py
create mode 100644 examples/auth/workos_oauth/README.md
create mode 100644 examples/auth/workos_oauth/client.py
create mode 100644 examples/auth/workos_oauth/server.py
rename src/fastmcp/server/auth/{proxy.py => oauth_proxy.py} (97%)
create mode 100644 src/fastmcp/server/auth/providers/azure.py
create mode 100644 tests/server/auth/__init__.py
create mode 100644 tests/server/auth/providers/__init__.py
create mode 100644 tests/server/auth/providers/test_azure.py
create mode 100644 tests/server/auth/providers/test_google.py
create mode 100644 tests/server/auth/providers/test_workos.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0af2b47eb..822641d16 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
hooks:
- id: ty
name: type check
- entry: ty check
+ entry: uv run ty check
language: system
types: [python]
files: ^src/|^tests/
@@ -34,7 +34,7 @@ repos:
require_serial: true
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v6.0.0
hooks:
- id: no-commit-to-branch
args: [--branch, main]
diff --git a/docs/deployment/self-hosted.mdx b/docs/deployment/self-hosted.mdx
index 12b7a5f11..34352f93d 100644
--- a/docs/deployment/self-hosted.mdx
+++ b/docs/deployment/self-hosted.mdx
@@ -117,7 +117,7 @@ If you already have a web application running, you can add MCP capabilities by m
For detailed integration guides, see:
- [FastAPI Integration](/integrations/fastapi)
-- [Starlette Integration](/integrations/starlette)
+- [ASGI / Starlette Integration](/integrations/asgi)
Here's a quick example showing how to add MCP to an existing FastAPI application:
diff --git a/docs/docs.json b/docs/docs.json
index 7d38cbbb8..f52693eda 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -4,10 +4,6 @@
"default": "system",
"strict": false
},
- "logo": {
- "light": "/assets/brand/logo-wordmark.svg",
- "dark": "/assets/brand/logo-wordmark-dark.svg"
- },
"background": {
"color": {
"dark": "#222831",
@@ -15,10 +11,6 @@
},
"decoration": "windows"
},
- "thumbnails": {
- "appearance": "light",
- "background": "/assets/brand/card-background.png"
- },
"banner": {
"content": "Remote MCP that just works: [FastMCP Cloud is here!](https://fastmcp.link/IhmBxWn) "
},
@@ -44,6 +36,10 @@
"measurementId": "G-64R5W1TJXG"
}
},
+ "logo": {
+ "dark": "/assets/brand/logo-wordmark-dark.svg",
+ "light": "/assets/brand/logo-wordmark.svg"
+ },
"name": "FastMCP",
"navbar": {
"primary": {
@@ -54,7 +50,6 @@
"navigation": {
"tabs": [
{
- "tab": "Documentation",
"anchors": [
{
"anchor": "Documentation",
@@ -157,23 +152,54 @@
{
"group": "Integrations",
"pages": [
- "integrations/anthropic",
- "integrations/authkit",
- "integrations/github",
- "integrations/google",
- "integrations/chatgpt",
- "integrations/claude-code",
- "integrations/claude-desktop",
- "integrations/cursor",
- "integrations/eunomia-authorization",
- "integrations/fastapi",
- "deployment/fastmcp-cloud",
- "integrations/gemini",
- "integrations/mcp-json-configuration",
- "integrations/openai",
- "integrations/openapi",
- "integrations/permit",
- "integrations/starlette"
+ {
+ "group": "Authentication",
+ "icon": "key",
+ "pages": [
+ "integrations/authkit",
+ "integrations/azure",
+ "integrations/github",
+ "integrations/google",
+ "integrations/workos-oauth"
+ ]
+ },
+ {
+ "group": "Authorization",
+ "icon": "shield-check",
+ "pages": [
+ "integrations/eunomia-authorization",
+ "integrations/permit"
+ ]
+ },
+ {
+ "group": "AI Assistants",
+ "icon": "robot",
+ "pages": [
+ "integrations/chatgpt",
+ "integrations/claude-code",
+ "integrations/claude-desktop",
+ "integrations/cursor",
+ "integrations/mcp-json-configuration"
+ ]
+ },
+ {
+ "group": "AI SDKs",
+ "icon": "code",
+ "pages": [
+ "integrations/anthropic",
+ "integrations/gemini",
+ "integrations/openai"
+ ]
+ },
+ {
+ "group": "Web Frameworks",
+ "icon": "globe",
+ "pages": [
+ "integrations/asgi",
+ "integrations/fastapi",
+ "integrations/openapi"
+ ]
+ }
]
},
{
@@ -205,10 +231,10 @@
"icon": "users",
"pages": ["community/showcase"]
}
- ]
+ ],
+ "tab": "Documentation"
},
{
- "tab": "SDK Reference",
"anchors": [
{
"anchor": "Python SDK",
@@ -287,11 +313,15 @@
"pages": [
"python-sdk/fastmcp-server-auth-__init__",
"python-sdk/fastmcp-server-auth-auth",
+ "python-sdk/fastmcp-server-auth-oauth_proxy",
{
"group": "providers",
"pages": [
"python-sdk/fastmcp-server-auth-providers-__init__",
+ "python-sdk/fastmcp-server-auth-providers-azure",
"python-sdk/fastmcp-server-auth-providers-bearer",
+ "python-sdk/fastmcp-server-auth-providers-github",
+ "python-sdk/fastmcp-server-auth-providers-google",
"python-sdk/fastmcp-server-auth-providers-in_memory",
"python-sdk/fastmcp-server-auth-providers-jwt",
"python-sdk/fastmcp-server-auth-providers-workos"
@@ -350,7 +380,8 @@
}
]
}
- ]
+ ],
+ "tab": "SDK Reference"
}
]
},
@@ -367,5 +398,9 @@
"search": {
"prompt": "Search the docs..."
},
- "theme": "mint"
+ "theme": "mint",
+ "thumbnails": {
+ "appearance": "light",
+ "background": "/assets/brand/card-background.png"
+ }
}
diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx
index 3d9a784bd..b8156ac95 100644
--- a/docs/integrations/anthropic.mdx
+++ b/docs/integrations/anthropic.mdx
@@ -1,7 +1,7 @@
---
title: Anthropic API 🤝 FastMCP
sidebarTitle: Anthropic API
-description: Call FastMCP servers from the Anthropic API
+description: Connect FastMCP servers to the Anthropic API
icon: message-code
---
diff --git a/docs/integrations/starlette.mdx b/docs/integrations/asgi.mdx
similarity index 98%
rename from docs/integrations/starlette.mdx
rename to docs/integrations/asgi.mdx
index 68378dea2..66e7e4055 100644
--- a/docs/integrations/starlette.mdx
+++ b/docs/integrations/asgi.mdx
@@ -1,6 +1,6 @@
---
-title: Starlette / ASGI 🤝 FastMCP
-sidebarTitle: Starlette / ASGI
+title: ASGI / Starlette 🤝 FastMCP
+sidebarTitle: ASGI / Starlette
description: Integrate FastMCP servers into ASGI applications
icon: server
---
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
new file mode 100644
index 000000000..ad8fc31a9
--- /dev/null
+++ b/docs/integrations/azure.mdx
@@ -0,0 +1,250 @@
+---
+title: Azure (Microsoft Entra) OAuth 🤝 FastMCP
+sidebarTitle: Azure
+description: Secure your FastMCP server with Azure/Microsoft Entra OAuth
+icon: microsoft
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Azure OAuth** (Microsoft Entra ID). Since Azure doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Azure's traditional OAuth with MCP's authentication requirements.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. An **[Azure Account](https://portal.azure.com/)** with access to create App registrations
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+3. Your Azure tenant ID (found in Azure Portal under Microsoft Entra ID)
+
+### Step 1: Create an Azure App Registration
+
+Create an App registration in Azure Portal to get the credentials needed for authentication:
+
+
+
+ Go to the [Azure Portal](https://portal.azure.com) and navigate to **Microsoft Entra ID → App registrations**.
+
+ Click **"New registration"** to create a new application.
+
+
+
+ Fill in the application details:
+
+ - **Name**: Choose a name users will recognize (e.g., "My FastMCP Server")
+ - **Supported account types**: Choose based on your needs:
+ - **Single tenant**: Only users in your organization
+ - **Multitenant**: Users in any Microsoft Entra directory
+ - **Multitenant + personal accounts**: Any Microsoft account
+ - **Redirect URI**: Select "Web" and enter your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
+
+
+ The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Azure allows `http://localhost` URLs. For production, you must use HTTPS.
+
+
+
+ If you want to use a custom callback path (e.g., `/auth/azure/callback`), make sure to set the same path in both your Azure App registration and the `redirect_path` parameter when configuring the AzureProvider.
+
+
+
+
+ After registration, navigate to **Certificates & secrets** in your app's settings.
+
+ - Click **"New client secret"**
+ - Add a description (e.g., "FastMCP Server")
+ - Choose an expiration period
+ - Click **"Add"**
+
+
+ Copy the secret value immediately - it won't be shown again! You'll need to create a new secret if you lose it.
+
+
+
+
+ From the **Overview** page of your app registration, note:
+
+ - **Application (client) ID**: A UUID like `835f09b6-0f0f-40cc-85cb-f32c5829a149`
+ - **Directory (tenant) ID**: A UUID like `08541b6e-646d-43de-a0eb-834e6713d6d5`
+ - **Client Secret**: The value you copied in the previous step
+
+
+ Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `AzureProvider`, which handles Azure's OAuth flow automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+# The AzureProvider handles Azure's token format and validation
+auth_provider = AzureProvider(
+ client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149", # Your Azure App Client ID
+ client_secret="your-client-secret", # Your Azure App Client Secret
+ tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5", # Your Azure Tenant ID (REQUIRED)
+ base_url="http://localhost:8000", # Must match your App registration
+ required_scopes=["User.Read", "email", "openid", "profile"], # Microsoft Graph permissions
+ # redirect_path="/auth/callback" # Default value, customize if needed
+)
+
+mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated Azure user."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ # The AzureProvider stores user data in token claims
+ return {
+ "azure_id": token.claims.get("sub"),
+ "email": token.claims.get("email"),
+ "name": token.claims.get("name"),
+ "job_title": token.claims.get("job_title"),
+ "office_location": token.claims.get("office_location")
+ }
+```
+
+
+**Important**: The `tenant_id` parameter is **REQUIRED**. Azure no longer supports using "common" for new applications due to security requirements. You must use one of:
+
+- **Your specific tenant ID**: Found in Azure Portal (e.g., `08541b6e-646d-43de-a0eb-834e6713d6d5`)
+- **"organizations"**: For work and school accounts only
+- **"consumers"**: For personal Microsoft accounts only
+
+Using your specific tenant ID is recommended for better security and control.
+
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by Azure OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your Azure-protected server:
+
+```python test_client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle Azure OAuth
+ async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
+ # First-time connection will open Azure login in your browser
+ print("✓ Authenticated with Azure!")
+
+ # Test the protected tool
+ result = await client.call_tool("get_user_info")
+ print(f"Azure user: {result['email']}")
+ print(f"Name: {result['name']}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to Microsoft's authorization page
+2. Sign in with your Microsoft account (work, school, or personal based on your tenant configuration)
+3. Grant the requested permissions
+4. After authorization, you'll be redirected back
+5. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Environment Variables
+
+For production deployments, use environment variables instead of hardcoding credentials.
+
+
+To use the registered Azure provider, you must set `FASTMCP_SERVER_AUTH=AZURE`. Learn more about [registered providers](/servers/auth/authentication#registered-providers).
+
+
+### Provider Selection
+
+
+Set to `AZURE` to use the registered AzureProvider with default configuration.
+
+
+### Azure-Specific Configuration
+
+
+
+Your Azure App registration Client ID (e.g., `835f09b6-0f0f-40cc-85cb-f32c5829a149`)
+
+
+
+Your Azure App registration Client Secret
+
+
+
+Your Azure tenant ID (specific ID, "organizations", or "consumers")
+
+
+This is **REQUIRED**. Find your tenant ID in Azure Portal under Microsoft Entra ID → Overview.
+
+
+
+
+Public URL of your FastMCP server for OAuth callbacks
+
+
+
+Redirect path configured in your Azure App registration
+
+
+
+Comma-separated list of required Microsoft Graph scopes
+
+
+
+HTTP request timeout for Microsoft Graph API calls
+
+
+
+Example `.env` file:
+```bash
+# Use the registered Azure provider
+FASTMCP_SERVER_AUTH=AZURE
+
+# Azure OAuth credentials
+FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID=835f09b6-0f0f-40cc-85cb-f32c5829a149
+FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET=your-client-secret-here
+FASTMCP_SERVER_AUTH_AZURE_TENANT_ID=08541b6e-646d-43de-a0eb-834e6713d6d5
+FASTMCP_SERVER_AUTH_AZURE_BASE_URL=https://your-server.com
+FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES=User.Read,email,profile
+```
+
+With environment variables set, your server code simplifies to:
+
+```python server.py
+from fastmcp import FastMCP
+
+# Authentication is automatically configured from environment
+mcp = FastMCP(name="Azure Secured App")
+
+@mcp.tool
+async def protected_tool(query: str) -> str:
+ """A tool that requires Azure authentication to access."""
+ # Your tool implementation here
+ return f"Processing authenticated request: {query}"
+```
+
diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx
index b75a7114b..20c42d93d 100644
--- a/docs/integrations/claude-desktop.mdx
+++ b/docs/integrations/claude-desktop.mdx
@@ -1,7 +1,7 @@
---
title: Claude Desktop 🤝 FastMCP
sidebarTitle: Claude Desktop
-description: Call FastMCP servers from Claude Desktop
+description: Connect FastMCP servers to Claude Desktop
icon: message-smile
---
diff --git a/docs/integrations/gemini.mdx b/docs/integrations/gemini.mdx
index 37c37fd72..159e662a5 100644
--- a/docs/integrations/gemini.mdx
+++ b/docs/integrations/gemini.mdx
@@ -1,7 +1,7 @@
---
title: Gemini SDK 🤝 FastMCP
sidebarTitle: Gemini SDK
-description: Call FastMCP servers from the Google Gemini SDK
+description: Connect FastMCP servers to the Google Gemini SDK
icon: message-code
---
diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx
index fc73c5a25..b849e1bd4 100644
--- a/docs/integrations/github.mdx
+++ b/docs/integrations/github.mdx
@@ -1,6 +1,6 @@
---
title: GitHub OAuth 🤝 FastMCP
-sidebarTitle: GitHub OAuth
+sidebarTitle: GitHub
description: Secure your FastMCP server with GitHub OAuth
icon: github
tag: NEW
@@ -36,10 +36,10 @@ Create an OAuth App in your GitHub settings to get the credentials needed for au
- **Application name**: Choose a name users will recognize (e.g., "My FastMCP Server")
- **Homepage URL**: Your application's homepage or documentation URL
- - **Authorization callback URL**: Your server URL + `/oauth/callback` (e.g., `http://localhost:8000/oauth/callback`)
+ - **Authorization callback URL**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
- The callback URL must match exactly. The default path is `/oauth/callback`, but you can customize it using the `redirect_path` parameter. For local development, GitHub allows `http://localhost` URLs. For production, you must use HTTPS.
+ The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, GitHub allows `http://localhost` URLs. For production, you must use HTTPS.
@@ -72,7 +72,7 @@ auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID
client_secret="github_pat_...", # Your GitHub OAuth App Client Secret
base_url="http://localhost:8000", # Must match your OAuth App configuration
- # redirect_path="/oauth/callback" # Default value, customize if needed
+ # redirect_path="/auth/callback" # Default value, customize if needed
)
mcp = FastMCP(name="GitHub Secured App", auth=auth_provider)
@@ -164,7 +164,7 @@ Your GitHub OAuth App Client Secret
Public URL of your FastMCP server for OAuth callbacks
-
+
Redirect path configured in your GitHub OAuth App
diff --git a/docs/integrations/google.mdx b/docs/integrations/google.mdx
index 78b07c553..14a79a80c 100644
--- a/docs/integrations/google.mdx
+++ b/docs/integrations/google.mdx
@@ -1,6 +1,6 @@
---
title: Google OAuth 🤝 FastMCP
-sidebarTitle: Google OAuth
+sidebarTitle: Google
description: Secure your FastMCP server with Google OAuth
icon: google
tag: NEW
@@ -39,10 +39,10 @@ Create an OAuth 2.0 Client ID in your Google Cloud Console to get the credential
- **Application type**: Web application
- **Name**: Choose a descriptive name (e.g., "FastMCP Server")
- **Authorized JavaScript origins**: Add your server's base URL (e.g., `http://localhost:8000`)
- - **Authorized redirect URIs**: Add your server URL + `/oauth/callback` (e.g., `http://localhost:8000/oauth/callback`)
+ - **Authorized redirect URIs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
- The redirect URI must match exactly. The default path is `/oauth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Google allows `http://localhost` URLs with various ports. For production, you must use HTTPS.
+ The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Google allows `http://localhost` URLs with various ports. For production, you must use HTTPS.
@@ -78,7 +78,7 @@ auth_provider = GoogleProvider(
client_secret="GOCSPX-abc123...", # Your Google OAuth Client Secret
base_url="http://localhost:8000", # Must match your OAuth configuration
required_scopes=["openid", "email", "profile"], # Request user information
- # redirect_path="/oauth/callback" # Default value, customize if needed
+ # redirect_path="/auth/callback" # Default value, customize if needed
)
mcp = FastMCP(name="Google Secured App", auth=auth_provider)
@@ -174,7 +174,7 @@ Your Google OAuth 2.0 Client Secret (e.g., `GOCSPX-abc123...`)
Public URL of your FastMCP server for OAuth callbacks
-
+
Redirect path configured in your Google OAuth Client
diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx
index b75787228..6f88193f5 100644
--- a/docs/integrations/openai.mdx
+++ b/docs/integrations/openai.mdx
@@ -1,7 +1,7 @@
---
title: OpenAI API 🤝 FastMCP
sidebarTitle: OpenAI API
-description: Call FastMCP servers from the OpenAI API
+description: Connect FastMCP servers to the OpenAI API
icon: message-code
---
diff --git a/docs/integrations/workos-oauth.mdx b/docs/integrations/workos-oauth.mdx
new file mode 100644
index 000000000..34e9198f4
--- /dev/null
+++ b/docs/integrations/workos-oauth.mdx
@@ -0,0 +1,240 @@
+---
+title: WorkOS 🤝 FastMCP
+sidebarTitle: WorkOS
+description: Authenticate FastMCP servers with WorkOS Connect
+icon: shield-check
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Secure your FastMCP server with WorkOS Connect authentication. This integration uses the OAuth Proxy pattern to handle authentication through WorkOS Connect while maintaining compatibility with MCP clients.
+
+
+This guide covers WorkOS Connect applications. For Dynamic Client Registration (DCR) with AuthKit, see the [AuthKit integration](/integrations/authkit) instead.
+
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A **[WorkOS Account](https://workos.com/)** with access to create OAuth Apps
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create a WorkOS OAuth App
+
+Create an OAuth App in your WorkOS dashboard to get the credentials needed for authentication:
+
+
+
+In your WorkOS dashboard:
+1. Navigate to **Applications**
+2. Click **Create Application**
+3. Select **OAuth Application**
+4. Name your application
+
+
+
+In your OAuth application settings:
+1. Copy your **Client ID** (starts with `client_`)
+2. Click **Generate Client Secret** and save it securely
+3. Copy your **AuthKit Domain** (e.g., `https://your-app.authkit.app`)
+
+
+
+In the **Redirect URIs** section:
+- Add: `http://localhost:8000/auth/callback` (for development)
+- For production, add your server's public URL + `/auth/callback`
+
+
+The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `WorkOSProvider`:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import WorkOSProvider
+
+# Configure WorkOS OAuth
+auth = WorkOSProvider(
+ client_id="client_YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ authkit_domain="https://your-app.authkit.app",
+ base_url="http://localhost:8000",
+ required_scopes=["openid", "profile", "email"]
+)
+
+mcp = FastMCP("WorkOS Protected Server", auth=auth)
+
+@mcp.tool
+def protected_tool(message: str) -> str:
+ """This tool requires authentication."""
+ return f"Authenticated user says: {message}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by WorkOS OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your WorkOS-protected server:
+
+```python client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle WorkOS OAuth
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ # First-time connection will open WorkOS login in your browser
+ print("✓ Authenticated with WorkOS!")
+
+ # Test the protected tool
+ result = await client.call_tool("protected_tool", {"message": "Hello!"})
+ print(result)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to WorkOS's authorization page
+2. After you authorize the app, you'll be redirected back
+3. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Environment Variables
+
+For production deployments, use environment variables instead of hardcoding credentials. These variables provide default values when instantiating `WorkOSProvider()`.
+
+
+Setting `FASTMCP_SERVER_AUTH=WORKOS` automatically provisions WorkOS as the default auth provider for all FastMCP servers. This is optional - you can still manually instantiate `WorkOSProvider()` without it. Learn more about [registered providers](/servers/auth/authentication#registered-providers).
+
+
+### Automatic Provider Selection (Optional)
+
+
+
+Set to `WORKOS` to automatically provision WorkOS authentication for FastMCP servers without explicitly passing an auth parameter.
+
+
+
+### WorkOS-Specific Configuration
+
+
+
+Your WorkOS OAuth App Client ID (e.g., `client_01K33Y6GGS7T3AWMPJWKW42Y3Q`)
+
+
+
+Your WorkOS OAuth App Client Secret
+
+
+
+Your WorkOS AuthKit domain (e.g., `https://your-app.authkit.app`)
+
+
+
+Public URL of your FastMCP server for OAuth callbacks
+
+
+
+Redirect path configured in your WorkOS OAuth App
+
+
+
+List of required OAuth scopes (e.g., `["openid", "profile", "email"]`)
+
+
+
+HTTP request timeout for WorkOS API calls
+
+
+
+Example `.env` file:
+```bash
+# WorkOS OAuth credentials (always used as defaults)
+FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID=client_01K33Y6GGS7T3AWMPJWKW42Y3Q
+FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET=your_client_secret
+FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN=https://your-app.authkit.app
+FASTMCP_SERVER_AUTH_WORKOS_BASE_URL=https://your-server.com
+FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES=["openid","profile","email"]
+
+# Optional: Automatically provision WorkOS auth for all servers
+FASTMCP_SERVER_AUTH=WORKOS
+```
+
+With environment variables set, you can either:
+
+**Option 1: Manual instantiation (env vars provide defaults)**
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import WorkOSProvider
+
+# Env vars provide default values for WorkOSProvider()
+auth = WorkOSProvider() # Uses env var defaults
+mcp = FastMCP(name="WorkOS Protected Server", auth=auth)
+```
+
+**Option 2: Automatic provisioning (requires FASTMCP_SERVER_AUTH=WORKOS)**
+```python server.py
+from fastmcp import FastMCP
+
+# Auth is automatically provisioned from FASTMCP_SERVER_AUTH
+mcp = FastMCP(name="WorkOS Protected Server")
+```
+
+## Configuration Options
+
+
+
+WorkOS OAuth application client ID
+
+
+
+WorkOS OAuth application client secret
+
+
+
+Your WorkOS AuthKit domain URL (e.g., `https://your-app.authkit.app`)
+
+
+
+Your FastMCP server's public URL
+
+
+
+OAuth scopes to request
+
+
+
+OAuth callback path
+
+
+
+API request timeout
+
+
\ No newline at end of file
diff --git a/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx
index a0c1f88cf..d0fcfae56 100644
--- a/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx
+++ b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx
@@ -24,7 +24,7 @@ mount(self, server: MountedServer) -> None
Adds a mounted server as a source for prompts.
-#### `has_prompt`
+#### `has_prompt`
```python
has_prompt(self, key: str) -> bool
@@ -33,7 +33,7 @@ has_prompt(self, key: str) -> bool
Check if a prompt exists.
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, key: str) -> Prompt
@@ -42,7 +42,7 @@ get_prompt(self, key: str) -> Prompt
Get prompt by key.
-#### `get_prompts`
+#### `get_prompts`
```python
get_prompts(self) -> dict[str, Prompt]
@@ -51,7 +51,7 @@ get_prompts(self) -> dict[str, Prompt]
Gets the complete, unfiltered inventory of all prompts.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> list[Prompt]
@@ -60,7 +60,7 @@ list_prompts(self) -> list[Prompt]
Lists all prompts, applying protocol filtering.
-#### `add_prompt_from_fn`
+#### `add_prompt_from_fn`
```python
add_prompt_from_fn(self, fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, description: str | None = None, tags: set[str] | None = None) -> FunctionPrompt
@@ -69,7 +69,7 @@ add_prompt_from_fn(self, fn: Callable[..., PromptResult | Awaitable[PromptResult
Create a prompt from a function.
-#### `add_prompt`
+#### `add_prompt`
```python
add_prompt(self, prompt: Prompt) -> Prompt
@@ -78,7 +78,7 @@ add_prompt(self, prompt: Prompt) -> Prompt
Add a prompt to the manager.
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
diff --git a/docs/python-sdk/fastmcp-resources-resource_manager.mdx b/docs/python-sdk/fastmcp-resources-resource_manager.mdx
index 38ac4a5c0..3a4c7c59c 100644
--- a/docs/python-sdk/fastmcp-resources-resource_manager.mdx
+++ b/docs/python-sdk/fastmcp-resources-resource_manager.mdx
@@ -45,7 +45,7 @@ get_resource_templates(self) -> dict[str, ResourceTemplate]
Get all registered templates, keyed by URI template.
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self) -> list[Resource]
@@ -54,7 +54,7 @@ list_resources(self) -> list[Resource]
Lists all resources, applying protocol filtering.
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self) -> list[ResourceTemplate]
@@ -63,7 +63,7 @@ list_resource_templates(self) -> list[ResourceTemplate]
Lists all templates, applying protocol filtering.
-#### `add_resource_or_template_from_fn`
+#### `add_resource_or_template_from_fn`
```python
add_resource_or_template_from_fn(self, fn: Callable[..., Any], uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> Resource | ResourceTemplate
@@ -84,7 +84,7 @@ Add a resource or template to the manager from a function.
- returns the existing resource or template.
-#### `add_resource_from_fn`
+#### `add_resource_from_fn`
```python
add_resource_from_fn(self, fn: Callable[..., Any], uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> Resource
@@ -105,7 +105,7 @@ Add a resource to the manager from a function.
- returns the existing resource.
-#### `add_resource`
+#### `add_resource`
```python
add_resource(self, resource: Resource) -> Resource
@@ -119,7 +119,7 @@ will be used as the storage key. To overwrite it, call
Resource.model_copy(key=new_key) before calling this method.
-#### `add_template_from_fn`
+#### `add_template_from_fn`
```python
add_template_from_fn(self, fn: Callable[..., Any], uri_template: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> ResourceTemplate
@@ -128,7 +128,7 @@ add_template_from_fn(self, fn: Callable[..., Any], uri_template: str, name: str
Create a template from a function.
-#### `add_template`
+#### `add_template`
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
@@ -146,7 +146,7 @@ ResourceTemplate.model_copy(key=new_key) before calling this method.
- returns the existing template.
-#### `has_resource`
+#### `has_resource`
```python
has_resource(self, uri: AnyUrl | str) -> bool
@@ -155,7 +155,7 @@ has_resource(self, uri: AnyUrl | str) -> bool
Check if a resource exists.
-#### `get_resource`
+#### `get_resource`
```python
get_resource(self, uri: AnyUrl | str) -> Resource
@@ -170,7 +170,7 @@ Get resource by URI, checking concrete resources first, then templates.
- `NotFoundError`: If no resource or template matching the URI is found.
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: AnyUrl | str) -> str | bytes
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
new file mode 100644
index 000000000..1debc786a
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
@@ -0,0 +1,319 @@
+---
+title: oauth_proxy
+sidebarTitle: oauth_proxy
+---
+
+# `fastmcp.server.auth.oauth_proxy`
+
+
+OAuth Proxy Provider for FastMCP.
+
+This provider acts as a transparent proxy to an upstream OAuth Authorization Server,
+handling Dynamic Client Registration locally while forwarding all other OAuth flows.
+This enables authentication with upstream providers that don't support DCR or have
+restricted client registration policies.
+
+Key features:
+- Proxies authorization and token endpoints to upstream server
+- Implements local Dynamic Client Registration with fixed upstream credentials
+- Validates tokens using upstream JWKS
+- Maintains minimal local state for bookkeeping
+- Enhanced logging with request correlation
+
+This implementation is based on the OAuth 2.1 specification and is designed for
+production use with enterprise identity providers.
+
+
+## Classes
+
+### `ProxyDCRClient`
+
+
+Client for DCR proxy that accepts any localhost redirect URI.
+
+This special client class is critical for the OAuth proxy to work correctly
+with Dynamic Client Registration (DCR). Here's why it exists:
+
+Problem:
+--------
+When MCP clients use OAuth, they dynamically register with random localhost
+ports (e.g., http://localhost:55454/callback). The OAuth proxy needs to:
+1. Accept these dynamic redirect URIs from clients
+2. Use its own fixed redirect URI with the upstream provider (Google, GitHub, etc.)
+3. Forward the authorization code back to the client's dynamic URI
+
+Solution:
+---------
+This class overrides redirect_uri validation to accept ANY localhost URI,
+while the proxy internally uses its own fixed redirect URI with the upstream
+provider. This allows the flow to work even when clients reconnect with
+different ports or when tokens are cached.
+
+Without this class, clients would get "Redirect URI not registered" errors
+when trying to authenticate with cached tokens, because the stored client
+would have fixed redirect URIs that don't match the new dynamic port.
+
+
+**Methods:**
+
+#### `validate_redirect_uri`
+
+```python
+validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl
+```
+
+Accept any localhost redirect URI for DCR clients.
+
+Since we're acting as a proxy and clients register dynamically,
+we need to accept their localhost redirect URIs even though they're
+not pre-registered with us. This is essential for cached token
+scenarios where the client may reconnect with a different port.
+
+
+### `OAuthProxy`
+
+
+OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
+
+Purpose
+-------
+MCP clients expect OAuth providers to support Dynamic Client Registration (DCR),
+where clients can register themselves dynamically and receive unique credentials.
+Most enterprise IDPs (Google, GitHub, Azure AD, etc.) don't support DCR and require
+pre-registered OAuth applications with fixed credentials.
+
+This proxy bridges that gap by:
+- Presenting a full DCR-compliant OAuth interface to MCP clients
+- Translating DCR registration requests to use pre-configured upstream credentials
+- Proxying all OAuth flows to the upstream IDP with appropriate translations
+- Managing the state and security requirements of both protocols
+
+Architecture Overview
+--------------------
+The proxy maintains a single OAuth app registration with the upstream provider
+while allowing unlimited MCP clients to register and authenticate dynamically.
+It implements the complete OAuth 2.1 + DCR specification for clients while
+translating to whatever OAuth variant the upstream provider requires.
+
+Key Translation Challenges Solved
+---------------------------------
+1. Dynamic Client Registration:
+ - MCP clients expect to register dynamically and get unique credentials
+ - Upstream IDPs require pre-registered apps with fixed credentials
+ - Solution: Accept DCR requests, return shared upstream credentials
+
+2. Dynamic Redirect URIs:
+ - MCP clients use random localhost ports that change between sessions
+ - Upstream IDPs require fixed, pre-registered redirect URIs
+ - Solution: Use proxy's fixed callback URL with upstream, forward to client's dynamic URI
+
+3. Authorization Code Mapping:
+ - Upstream returns codes for the proxy's redirect URI
+ - Clients expect codes for their own redirect URIs
+ - Solution: Exchange upstream code server-side, issue new code to client
+
+4. State Parameter Collision:
+ - Both client and proxy need to maintain state through the flow
+ - Only one state parameter available in OAuth
+ - Solution: Use transaction ID as state with upstream, preserve client's state
+
+5. Token Management:
+ - Clients may expect different token formats/claims than upstream provides
+ - Need to track tokens for revocation and refresh
+ - Solution: Store token relationships, forward upstream tokens transparently
+
+OAuth Flow Implementation
+------------------------
+1. Client Registration (DCR):
+ - Accept any client registration request
+ - Store ProxyDCRClient that accepts dynamic redirect URIs
+ - Return shared upstream credentials to all clients
+
+2. Authorization:
+ - Store transaction mapping client details to proxy flow
+ - Redirect to upstream with proxy's fixed redirect URI
+ - Use transaction ID as state parameter with upstream
+
+3. Upstream Callback:
+ - Exchange upstream authorization code for tokens (server-side)
+ - Generate new authorization code bound to client's PKCE challenge
+ - Redirect to client's original dynamic redirect URI
+
+4. Token Exchange:
+ - Validate client's code and PKCE verifier
+ - Return previously obtained upstream tokens
+ - Clean up one-time use authorization code
+
+5. Token Refresh:
+ - Forward refresh requests to upstream using authlib
+ - Handle token rotation if upstream issues new refresh token
+ - Update local token mappings
+
+State Management
+---------------
+The proxy maintains minimal but crucial state:
+- _clients: DCR registrations (all use ProxyDCRClient for flexibility)
+- _oauth_transactions: Active authorization flows with client context
+- _client_codes: Authorization codes with PKCE challenges and upstream tokens
+- _access_tokens, _refresh_tokens: Token storage for revocation
+- Token relationship mappings for cleanup and rotation
+
+Security Considerations
+----------------------
+- PKCE enforced end-to-end (client to proxy, proxy to upstream)
+- Authorization codes are single-use with short expiry
+- Transaction IDs are cryptographically random
+- All state is cleaned up after use to prevent replay
+- Token validation delegates to upstream provider
+
+Provider Compatibility
+---------------------
+Works with any OAuth 2.0 provider that supports:
+- Authorization code flow
+- Fixed redirect URI (configured in provider's app settings)
+- Standard token endpoint
+
+Handles provider-specific requirements:
+- Google: Ensures minimum scope requirements
+- GitHub: Compatible with OAuth Apps and GitHub Apps
+- Azure AD: Handles tenant-specific endpoints
+- Generic: Works with any spec-compliant provider
+
+
+**Methods:**
+
+#### `get_client`
+
+```python
+get_client(self, client_id: str) -> OAuthClientInformationFull | None
+```
+
+Get client information by ID.
+
+For unregistered clients, returns a ProxyDCRClient that accepts
+any localhost redirect URI for DCR clients.
+
+Even registered clients use ProxyDCRClient to ensure they can
+authenticate with different dynamic ports on reconnection. This
+handles the case where a client with cached tokens reconnects
+on a different port.
+
+
+#### `register_client`
+
+```python
+register_client(self, client_info: OAuthClientInformationFull) -> None
+```
+
+Register a client locally using fixed upstream credentials.
+
+This implementation always uses the upstream client_id and client_secret
+regardless of what the client requests. It modifies the client_info object
+in place since the MCP framework ignores return values.
+
+This ensures all clients use the same credentials that are registered
+with the upstream server.
+
+Implementation Detail:
+We store a ProxyDCRClient (not the original client_info) to ensure
+the client can reconnect with different dynamic redirect URIs. This is
+essential for cached token scenarios where the client port changes.
+
+The flow:
+1. Client provides its desired redirect URIs (dynamic localhost ports)
+2. We create a ProxyDCRClient that will accept ANY localhost URI
+3. We store this flexible client for future authentications
+4. When client reconnects with a different port, ProxyDCRClient accepts it
+
+
+#### `authorize`
+
+```python
+authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
+```
+
+Start OAuth transaction and redirect to upstream IdP.
+
+This implements the DCR-compliant proxy pattern:
+1. Store transaction with client details and PKCE challenge
+2. Use transaction ID as state for IdP
+3. Redirect to IdP with our fixed callback URL
+
+
+#### `load_authorization_code`
+
+```python
+load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
+```
+
+Load authorization code for validation.
+
+Look up our client code and return authorization code object
+with PKCE challenge for validation.
+
+
+#### `exchange_authorization_code`
+
+```python
+exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
+```
+
+Exchange authorization code for stored IdP tokens.
+
+For the DCR-compliant proxy flow, we return the IdP tokens that were obtained
+during the IdP callback exchange. PKCE validation is handled by the MCP framework.
+
+
+#### `load_refresh_token`
+
+```python
+load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
+```
+
+Load refresh token from local storage.
+
+
+#### `exchange_refresh_token`
+
+```python
+exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
+```
+
+Exchange refresh token for new access token using authlib.
+
+
+#### `load_access_token`
+
+```python
+load_access_token(self, token: str) -> AccessToken | None
+```
+
+Validate access token using upstream JWKS.
+
+Delegates to the JWT verifier which handles signature validation,
+expiration checking, and claims validation using the upstream JWKS.
+
+
+#### `revoke_token`
+
+```python
+revoke_token(self, token: AccessToken | RefreshToken) -> None
+```
+
+Revoke token locally and with upstream server if supported.
+
+Removes tokens from local storage and attempts to revoke them with
+the upstream server if a revocation endpoint is configured.
+
+
+#### `get_routes`
+
+```python
+get_routes(self) -> list[Route]
+```
+
+Get OAuth routes with custom proxy token handler.
+
+This method creates standard OAuth routes and replaces the token endpoint
+with our proxy handler that forwards requests to the upstream OAuth server.
+
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
new file mode 100644
index 000000000..559f1238a
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
@@ -0,0 +1,63 @@
+---
+title: azure
+sidebarTitle: azure
+---
+
+# `fastmcp.server.auth.providers.azure`
+
+
+Azure (Microsoft Entra) OAuth provider for FastMCP.
+
+This provider implements Azure/Microsoft Entra ID OAuth authentication
+using the OAuth Proxy pattern for non-DCR OAuth flows.
+
+
+## Classes
+
+### `AzureProviderSettings`
+
+
+Settings for Azure OAuth provider.
+
+
+### `AzureTokenVerifier`
+
+
+Token verifier for Azure OAuth tokens.
+
+Azure tokens are JWTs, but we verify them by calling the Microsoft Graph API
+to get user information and validate the token.
+
+
+**Methods:**
+
+#### `verify_token`
+
+```python
+verify_token(self, token: str) -> AccessToken | None
+```
+
+Verify Azure OAuth token by calling Microsoft Graph API.
+
+
+### `AzureProvider`
+
+
+Azure (Microsoft Entra) OAuth provider for FastMCP.
+
+This provider implements Azure/Microsoft Entra ID authentication using the
+OAuth Proxy pattern. It supports both organizational accounts and personal
+Microsoft accounts depending on the tenant configuration.
+
+Features:
+- Transparent OAuth proxy to Azure/Microsoft identity platform
+- Automatic token validation via Microsoft Graph API
+- User information extraction
+- Support for different tenant configurations (common, organizations, consumers)
+
+Setup Requirements:
+1. Register an application in Azure Portal (portal.azure.com)
+2. Configure redirect URI as: http://localhost:8000/auth/callback
+3. Note your Application (client) ID and create a client secret
+4. Optionally note your Directory (tenant) ID for single-tenant apps
+
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
new file mode 100644
index 000000000..72273c01a
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
@@ -0,0 +1,72 @@
+---
+title: github
+sidebarTitle: github
+---
+
+# `fastmcp.server.auth.providers.github`
+
+
+GitHub OAuth provider for FastMCP.
+
+This module provides a complete GitHub OAuth integration that's ready to use
+with just a client ID and client secret. It handles all the complexity of
+GitHub's OAuth flow, token validation, and user management.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.auth.providers.github import GitHubProvider
+
+ # Simple GitHub OAuth protection
+ auth = GitHubProvider(
+ client_id="your-github-client-id",
+ client_secret="your-github-client-secret"
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+
+
+## Classes
+
+### `GitHubProviderSettings`
+
+
+Settings for GitHub OAuth provider.
+
+
+### `GitHubTokenVerifier`
+
+
+Token verifier for GitHub OAuth tokens.
+
+GitHub OAuth tokens are opaque (not JWTs), so we verify them
+by calling GitHub's API to check if they're valid and get user info.
+
+
+**Methods:**
+
+#### `verify_token`
+
+```python
+verify_token(self, token: str) -> AccessToken | None
+```
+
+Verify GitHub OAuth token by calling GitHub API.
+
+
+### `GitHubProvider`
+
+
+Complete GitHub OAuth provider for FastMCP.
+
+This provider makes it trivial to add GitHub OAuth protection to any
+FastMCP server. Just provide your GitHub OAuth app credentials and
+a base URL, and you're ready to go.
+
+Features:
+- Transparent OAuth proxy to GitHub
+- Automatic token validation via GitHub API
+- User information extraction
+- Minimal configuration required
+
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
new file mode 100644
index 000000000..f7bbd5ee0
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
@@ -0,0 +1,72 @@
+---
+title: google
+sidebarTitle: google
+---
+
+# `fastmcp.server.auth.providers.google`
+
+
+Google OAuth provider for FastMCP.
+
+This module provides a complete Google OAuth integration that's ready to use
+with just a client ID and client secret. It handles all the complexity of
+Google's OAuth flow, token validation, and user management.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.auth.providers.google import GoogleProvider
+
+ # Simple Google OAuth protection
+ auth = GoogleProvider(
+ client_id="your-google-client-id.apps.googleusercontent.com",
+ client_secret="your-google-client-secret"
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+
+
+## Classes
+
+### `GoogleProviderSettings`
+
+
+Settings for Google OAuth provider.
+
+
+### `GoogleTokenVerifier`
+
+
+Token verifier for Google OAuth tokens.
+
+Google OAuth tokens are opaque (not JWTs), so we verify them
+by calling Google's tokeninfo API to check if they're valid and get user info.
+
+
+**Methods:**
+
+#### `verify_token`
+
+```python
+verify_token(self, token: str) -> AccessToken | None
+```
+
+Verify Google OAuth token by calling Google's tokeninfo API.
+
+
+### `GoogleProvider`
+
+
+Complete Google OAuth provider for FastMCP.
+
+This provider makes it trivial to add Google OAuth protection to any
+FastMCP server. Just provide your Google OAuth app credentials and
+a base URL, and you're ready to go.
+
+Features:
+- Transparent OAuth proxy to Google
+- Automatic token validation via Google's tokeninfo API
+- User information extraction from Google APIs
+- Minimal configuration required
+
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
index 48e965b49..f391c4307 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
@@ -5,11 +5,69 @@ sidebarTitle: workos
# `fastmcp.server.auth.providers.workos`
+
+WorkOS authentication providers for FastMCP.
+
+This module provides two WorkOS authentication strategies:
+
+1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)
+2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit
+
+Choose based on your WorkOS setup and authentication requirements.
+
+
## Classes
-### `AuthKitProviderSettings`
+### `WorkOSProviderSettings`
-### `AuthKitProvider`
+
+Settings for WorkOS OAuth provider.
+
+
+### `WorkOSTokenVerifier`
+
+
+Token verifier for WorkOS OAuth tokens.
+
+WorkOS AuthKit tokens are opaque, so we verify them by calling
+the /oauth2/userinfo endpoint to check validity and get user info.
+
+
+**Methods:**
+
+#### `verify_token`
+
+```python
+verify_token(self, token: str) -> AccessToken | None
+```
+
+Verify WorkOS OAuth token by calling userinfo endpoint.
+
+
+### `WorkOSProvider`
+
+
+Complete WorkOS OAuth provider for FastMCP.
+
+This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.
+It provides OAuth2 authentication for users through WorkOS Connect applications.
+
+Features:
+- Transparent OAuth proxy to WorkOS AuthKit
+- Automatic token validation via userinfo endpoint
+- User information extraction from ID tokens
+- Support for standard OAuth scopes (openid, profile, email)
+
+Setup Requirements:
+1. Create a WorkOS Connect application in your dashboard
+2. Note your AuthKit domain (e.g., "https://your-app.authkit.app")
+3. Configure redirect URI as: http://localhost:8000/auth/callback
+4. Note your Client ID and Client Secret
+
+
+### `AuthKitProviderSettings`
+
+### `AuthKitProvider`
AuthKit metadata provider for DCR (Dynamic Client Registration).
@@ -35,7 +93,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx
index 6fe769771..4fcd592a6 100644
--- a/docs/python-sdk/fastmcp-server-server.mdx
+++ b/docs/python-sdk/fastmcp-server-server.mdx
@@ -278,7 +278,7 @@ The handler function must be an async function that accepts a Starlette
Request and returns a Response.
**Args:**
-- `path`: URL path for the route (e.g., "/oauth/callback")
+- `path`: URL path for the route (e.g., "/auth/callback")
- `methods`: List of HTTP methods to support (e.g., ["GET", "POST"])
- `name`: Optional name for the route (to reference this route with
Starlette's reverse URL lookup feature)
diff --git a/docs/python-sdk/fastmcp-tools-tool_manager.mdx b/docs/python-sdk/fastmcp-tools-tool_manager.mdx
index 99a6d9774..15cbee1db 100644
--- a/docs/python-sdk/fastmcp-tools-tool_manager.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool_manager.mdx
@@ -24,7 +24,7 @@ mount(self, server: MountedServer) -> None
Adds a mounted server as a source for tools.
-#### `has_tool`
+#### `has_tool`
```python
has_tool(self, key: str) -> bool
@@ -33,7 +33,7 @@ has_tool(self, key: str) -> bool
Check if a tool exists.
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, key: str) -> Tool
@@ -42,7 +42,7 @@ get_tool(self, key: str) -> Tool
Get tool by key.
-#### `get_tools`
+#### `get_tools`
```python
get_tools(self) -> dict[str, Tool]
@@ -51,7 +51,7 @@ get_tools(self) -> dict[str, Tool]
Gets the complete, unfiltered inventory of all tools.
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self) -> list[Tool]
@@ -60,7 +60,7 @@ list_tools(self) -> list[Tool]
Lists all tools, applying protocol filtering.
-#### `add_tool_from_fn`
+#### `add_tool_from_fn`
```python
add_tool_from_fn(self, fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, exclude_args: list[str] | None = None) -> Tool
@@ -69,7 +69,7 @@ add_tool_from_fn(self, fn: Callable[..., Any], name: str | None = None, descript
Add a tool to the server.
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self, tool: Tool) -> Tool
@@ -78,7 +78,7 @@ add_tool(self, tool: Tool) -> Tool
Register a tool with the server.
-#### `add_tool_transformation`
+#### `add_tool_transformation`
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@@ -87,7 +87,7 @@ add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfi
Add a tool transformation.
-#### `get_tool_transformation`
+#### `get_tool_transformation`
```python
get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None
@@ -96,7 +96,7 @@ get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None
Get a tool transformation.
-#### `remove_tool_transformation`
+#### `remove_tool_transformation`
```python
remove_tool_transformation(self, tool_name: str) -> None
@@ -105,7 +105,7 @@ remove_tool_transformation(self, tool_name: str) -> None
Remove a tool transformation.
-#### `remove_tool`
+#### `remove_tool`
```python
remove_tool(self, key: str) -> None
@@ -120,7 +120,7 @@ Remove a tool from the server.
- `NotFoundError`: If the tool is not found
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult
diff --git a/docs/python-sdk/fastmcp-utilities-tests.mdx b/docs/python-sdk/fastmcp-utilities-tests.mdx
index 13d156215..ce6d3d761 100644
--- a/docs/python-sdk/fastmcp-utilities-tests.mdx
+++ b/docs/python-sdk/fastmcp-utilities-tests.mdx
@@ -35,13 +35,15 @@ returns the server URL. When the context manager is exited, the server process i
not pickleable, so we need a function that creates and runs one.
- `*args`: Arguments to pass to the server function.
- `provide_host_and_port`: Whether to provide the host and port to the server function as kwargs.
+- `host`: Host to bind the server to (default\: "127.0.0.1").
+- `port`: Port to bind the server to (default\: find available port).
- `**kwargs`: Keyword arguments to pass to the server function.
**Returns:**
- The server URL.
-### `caplog_for_fastmcp`
+### `caplog_for_fastmcp`
```python
caplog_for_fastmcp(caplog)
@@ -53,7 +55,7 @@ Context manager to capture logs from FastMCP loggers even when propagation is di
## Classes
-### `HeadlessOAuth`
+### `HeadlessOAuth`
OAuth provider that bypasses browser interaction for testing.
@@ -64,7 +66,7 @@ instead of opening a browser and running a callback server. Useful for automated
**Methods:**
-#### `redirect_handler`
+#### `redirect_handler`
```python
redirect_handler(self, authorization_url: str) -> None
@@ -73,7 +75,7 @@ redirect_handler(self, authorization_url: str) -> None
Make HTTP request to authorization URL and store response for callback handler.
-#### `callback_handler`
+#### `callback_handler`
```python
callback_handler(self) -> tuple[str, str | None]
diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index d55dc4ac7..435168cfd 100644
--- a/docs/servers/auth/oauth-proxy.mdx
+++ b/docs/servers/auth/oauth-proxy.mdx
@@ -75,10 +75,10 @@ sequenceDiagram
Note over Client, Proxy: Authorization with Callback Forwarding
Client->>Proxy: 3. GET /authorize
redirect_uri=localhost:54321/callback
Note over Proxy: Store transaction with client callback
- Proxy->>Provider: 4. Redirect to provider
redirect_uri=server:8000/oauth/callback
+ Proxy->>Provider: 4. Redirect to provider
redirect_uri=server:8000/auth/callback
Note over Provider, Proxy: Provider Callback
- Provider->>Proxy: 5. GET /oauth/callback
with authorization code
+ Provider->>Proxy: 5. GET /auth/callback
with authorization code
Proxy->>Provider: 6. Exchange code for tokens
Provider-->>Proxy: 7. Access & refresh tokens
@@ -142,7 +142,7 @@ The `OAuthProxy` class provides the complete proxy implementation:
Public URL of your FastMCP server (e.g., `https://your-server.com`)
-
+
Path for OAuth callbacks. Must match the redirect URI configured in your OAuth application
@@ -191,8 +191,8 @@ auth = OAuthProxy(
# Your FastMCP server URL (string automatically converted to AnyHttpUrl)
base_url="https://your-server.com",
- # Optional: customize callback path (defaults to "/oauth/callback")
- redirect_path="/oauth/callback"
+ # Optional: customize callback path (defaults to "/auth/callback")
+ redirect_path="/auth/callback"
)
mcp = FastMCP(name="My Server", auth=auth)
@@ -203,13 +203,13 @@ mcp = FastMCP(name="My Server", auth=auth)
When registering your application with your OAuth provider, configure the redirect/callback URL as:
```
-https://your-server.com/oauth/callback
+https://your-server.com/auth/callback
```
For local development with providers that support it (like GitHub):
```
-http://localhost:8000/oauth/callback
+http://localhost:8000/auth/callback
```
The proxy automatically:
diff --git a/examples/auth/azure_oauth/README.md b/examples/auth/azure_oauth/README.md
new file mode 100644
index 000000000..ba0757ca7
--- /dev/null
+++ b/examples/auth/azure_oauth/README.md
@@ -0,0 +1,51 @@
+# Azure (Microsoft Entra) OAuth Example
+
+This example demonstrates how to use the Azure OAuth provider with FastMCP servers.
+
+## Setup
+
+### 1. Azure App Registration
+
+1. Go to [Azure Portal → App registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)
+2. Click "New registration" and configure:
+ - Name: Your app name
+ - Supported account types: Choose based on your needs
+ - Redirect URI: `http://localhost:8000/auth/callback` (Web platform)
+3. After creation, go to "Certificates & secrets" → "New client secret"
+4. Note these values from the Overview page:
+ - Application (client) ID
+ - Directory (tenant) ID
+
+### 2. Environment Variables
+
+Create a `.env` file:
+
+```bash
+# Required
+AZURE_CLIENT_ID=your-application-client-id
+AZURE_CLIENT_SECRET=your-client-secret-value
+AZURE_TENANT_ID=your-tenant-id # From Azure Portal Overview page
+```
+
+### 3. Run the Example
+
+Start the server:
+
+```bash
+uv run python server.py
+```
+
+Test with client:
+
+```bash
+uv run python client.py
+```
+
+## Tenant Configuration
+
+The `tenant_id` parameter is **required** and controls which accounts can authenticate:
+
+- **Your tenant ID**: Single organization (most common)
+- **`organizations`**: Any work/school account
+- **`consumers`**: Personal Microsoft accounts only
+
diff --git a/examples/auth/azure_oauth/client.py b/examples/auth/azure_oauth/client.py
new file mode 100644
index 000000000..5f1f39bb2
--- /dev/null
+++ b/examples/auth/azure_oauth/client.py
@@ -0,0 +1,32 @@
+"""OAuth client example for connecting to FastMCP servers.
+
+This example demonstrates how to connect to an OAuth-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp.client import Client
+
+SERVER_URL = "http://127.0.0.1:8000/mcp"
+
+
+async def main():
+ try:
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("✅ Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"🔧 Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+ except Exception as e:
+ print(f"❌ Authentication failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/azure_oauth/server.py b/examples/auth/azure_oauth/server.py
new file mode 100644
index 000000000..2d5062612
--- /dev/null
+++ b/examples/auth/azure_oauth/server.py
@@ -0,0 +1,39 @@
+"""Azure (Microsoft Entra) OAuth server example for FastMCP.
+
+This example demonstrates how to protect a FastMCP server with Azure/Microsoft OAuth.
+
+Required environment variables:
+- AZURE_CLIENT_ID: Your Azure application (client) ID
+- AZURE_CLIENT_SECRET: Your Azure client secret
+- AZURE_TENANT_ID: Tenant ID
+ Options: "organizations" (work/school), "consumers" (personal), or specific tenant ID
+
+To run:
+ python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+auth = AzureProvider(
+ client_id=os.getenv("AZURE_CLIENT_ID") or "",
+ client_secret=os.getenv("AZURE_CLIENT_SECRET") or "",
+ tenant_id=os.getenv("AZURE_TENANT_ID")
+ or "", # Required for single-tenant apps - get from Azure Portal
+ base_url="http://localhost:8000",
+ # redirect_path="/auth/callback", # Default path - change if using a different callback URL
+)
+
+mcp = FastMCP("Azure OAuth Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/examples/auth/github_oauth/README.md b/examples/auth/github_oauth/README.md
index f3838d0be..557ba7774 100644
--- a/examples/auth/github_oauth/README.md
+++ b/examples/auth/github_oauth/README.md
@@ -6,7 +6,7 @@ Demonstrates FastMCP server protection with GitHub OAuth.
1. Create a GitHub OAuth App:
- Go to GitHub Settings > Developer settings > OAuth Apps
- - Set Authorization callback URL to: `http://localhost:8000/oauth/callback`
+ - Set Authorization callback URL to: `http://localhost:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/github_oauth/server.py b/examples/auth/github_oauth/server.py
index 1dd8051d2..1f88c6977 100644
--- a/examples/auth/github_oauth/server.py
+++ b/examples/auth/github_oauth/server.py
@@ -19,7 +19,7 @@ auth = GitHubProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "",
base_url="http://localhost:8000",
- # redirect_path="/oauth/callback", # Default path - change if using a different callback URL
+ # redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
mcp = FastMCP("GitHub OAuth Example Server", auth=auth)
diff --git a/examples/auth/google_oauth/README.md b/examples/auth/google_oauth/README.md
index e23728435..869718344 100644
--- a/examples/auth/google_oauth/README.md
+++ b/examples/auth/google_oauth/README.md
@@ -9,7 +9,7 @@ Demonstrates FastMCP server protection with Google OAuth.
- Create or select a project
- Go to APIs & Services > Credentials
- Create OAuth 2.0 Client ID (Web application)
- - Add Authorized redirect URI: `http://localhost:8000/oauth/callback`
+ - Add Authorized redirect URI: `http://localhost:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/google_oauth/server.py b/examples/auth/google_oauth/server.py
index 65f96f1f0..2a5b1c7df 100644
--- a/examples/auth/google_oauth/server.py
+++ b/examples/auth/google_oauth/server.py
@@ -19,7 +19,7 @@ auth = GoogleProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET") or "",
base_url="http://localhost:8000",
- # redirect_path="/oauth/callback", # Default path - change if using a different callback URL
+ # redirect_path="/auth/callback", # Default path - change if using a different callback URL
# Optional: specify required scopes
# required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
)
diff --git a/examples/auth/workos_oauth/README.md b/examples/auth/workos_oauth/README.md
new file mode 100644
index 000000000..359e5b904
--- /dev/null
+++ b/examples/auth/workos_oauth/README.md
@@ -0,0 +1,159 @@
+# WorkOS OAuth Example
+
+This example demonstrates how to use the WorkOS OAuth provider with FastMCP servers.
+
+## Overview
+
+The WorkOS OAuth provider enables authentication using WorkOS User Management. It provides general OAuth2 authentication similar to GitHub or Google, with optional support for enterprise SSO connections. Unlike the AuthKit provider which uses DCR (Dynamic Client Registration), this provider works with traditional OAuth flows.
+
+## Setup
+
+### 1. WorkOS Configuration
+
+1. **Create a WorkOS Application**:
+ - Go to [WorkOS Dashboard → Applications](https://dashboard.workos.com/applications)
+ - Create a new application or use an existing one
+ - Enable **User Management** for OAuth authentication
+ - Copy your `Client ID` and `API Key` (client secret)
+
+2. **Configure SSO Connection** (optional for enterprise SSO):
+ - Go to WorkOS Dashboard → Connections
+ - Set up your SSO connection (SAML, OIDC, or OAuth provider like Google/Microsoft)
+ - Note the `Organization ID` or `Connection ID` if using SSO
+
+3. **Set Redirect URLs**:
+ - In your WorkOS application settings, add redirect URLs for your OAuth flow
+ - For this example: `http://localhost:8000/auth/callback`
+
+### 2. Environment Variables
+
+Create a `.env` file in this directory:
+
+```bash
+# Required WorkOS credentials
+WORKOS_CLIENT_ID=client_123
+WORKOS_API_KEY=sk_test_456 # Your WorkOS API key (client secret)
+
+# Server URL (optional, defaults to http://localhost:8000)
+# WORKOS_BASE_URL=http://localhost:8000
+
+# Optional: For enterprise SSO connections
+# WORKOS_ORGANIZATION_ID=org_123 # Route to specific organization's SSO
+# WORKOS_CONNECTION_ID=conn_456 # Route to specific SSO connection
+
+# Optional: Required scopes
+# FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES=["profile", "email"]
+```
+
+### 3. Install Dependencies
+
+```bash
+cd /Users/jlowin/Developer/fastmcp
+uv sync
+```
+
+## Running the Example
+
+### Start the Server
+
+```bash
+# From this directory
+uv run python server.py
+```
+
+The server will start on `http://localhost:8000` with WorkOS OAuth authentication enabled.
+
+### Test with Client
+
+In another terminal:
+
+```bash
+# From this directory
+uv run python client.py
+```
+
+The client will:
+1. Attempt to connect to the server
+2. Detect that OAuth authentication is required
+3. Open a browser for WorkOS authentication
+4. Complete the OAuth flow and connect to the server
+5. Demonstrate calling authenticated tools
+
+## How It Works
+
+### Authentication Flow
+
+1. **Client Request**: Client attempts to connect to FastMCP server
+2. **Auth Challenge**: Server responds with `401 Unauthorized` and `WWW-Authenticate` header
+3. **OAuth Discovery**: Client discovers OAuth endpoints from server metadata
+4. **Authorization**: Client redirects user to WorkOS for authentication
+5. **Callback**: WorkOS redirects back with authorization code
+6. **Token Exchange**: Client exchanges code for access token
+7. **API Calls**: Client uses access token for authenticated MCP requests
+
+### Server Components
+
+- **WorkOSProvider**: Validates tokens using WorkOS User Management API
+- **Protected Resources**: MCP tools and resources require valid WorkOS tokens
+- **OAuth Metadata**: Server advertises WorkOS as authorization server
+
+### Client Components
+
+- **OAuth Client**: Handles browser-based OAuth flow
+- **Token Storage**: Caches tokens for future use
+- **Automatic Auth**: Transparently handles authentication
+
+## Key Features
+
+- **SSO Integration**: Works with any WorkOS SSO connection
+- **User Management**: Validates tokens against WorkOS User Management API
+- **Token Caching**: Reuses tokens across sessions
+- **Error Handling**: Graceful handling of auth failures and token expiration
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"Invalid client" error**: Check CLIENT_ID and CLIENT_SECRET
+2. **"Token validation failed"**: Check API_KEY and token scope
+3. **"Redirect URI mismatch"**: Ensure redirect URL matches WorkOS settings
+4. **Browser doesn't open**: Check firewall settings for localhost
+
+### Debug Mode
+
+Enable debug logging:
+
+```python
+import logging
+logging.basicConfig(level=logging.DEBUG)
+```
+
+### Token Inspection
+
+Check cached tokens:
+
+```bash
+ls ~/.fastmcp/oauth-mcp-client-cache/
+```
+
+Clear token cache:
+
+```python
+from fastmcp.client.auth.oauth import FileTokenStorage
+FileTokenStorage.clear_all()
+```
+
+## Security Notes
+
+- Never commit `.env` files with real credentials
+- Use HTTPS in production
+- Rotate API keys regularly
+- Monitor WorkOS logs for unusual activity
+- Set appropriate token expiration times
+
+## Next Steps
+
+- Explore WorkOS Directory Sync for user provisioning
+- Set up multi-organization support
+- Implement role-based access control
+- Add custom scopes and claims validation
\ No newline at end of file
diff --git a/examples/auth/workos_oauth/client.py b/examples/auth/workos_oauth/client.py
new file mode 100644
index 000000000..5f1f39bb2
--- /dev/null
+++ b/examples/auth/workos_oauth/client.py
@@ -0,0 +1,32 @@
+"""OAuth client example for connecting to FastMCP servers.
+
+This example demonstrates how to connect to an OAuth-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp.client import Client
+
+SERVER_URL = "http://127.0.0.1:8000/mcp"
+
+
+async def main():
+ try:
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("✅ Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"🔧 Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+ except Exception as e:
+ print(f"❌ Authentication failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/workos_oauth/server.py b/examples/auth/workos_oauth/server.py
new file mode 100644
index 000000000..08c1db62b
--- /dev/null
+++ b/examples/auth/workos_oauth/server.py
@@ -0,0 +1,37 @@
+"""WorkOS OAuth server example for FastMCP.
+
+This example demonstrates how to protect a FastMCP server with WorkOS OAuth.
+
+Required environment variables:
+- WORKOS_CLIENT_ID: Your WorkOS Connect application client ID
+- WORKOS_CLIENT_SECRET: Your WorkOS Connect application client secret
+- WORKOS_AUTHKIT_DOMAIN: Your AuthKit domain (e.g., "https://your-app.authkit.app")
+
+To run:
+ python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import WorkOSProvider
+
+auth = WorkOSProvider(
+ client_id=os.getenv("WORKOS_CLIENT_ID") or "",
+ client_secret=os.getenv("WORKOS_CLIENT_SECRET") or "",
+ authkit_domain=os.getenv("WORKOS_AUTHKIT_DOMAIN") or "https://your-app.authkit.app",
+ base_url="http://localhost:8000",
+ # redirect_path="/auth/callback", # Default path - change if using a different callback URL
+)
+
+mcp = FastMCP("WorkOS OAuth Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/src/fastmcp/server/auth/proxy.py b/src/fastmcp/server/auth/oauth_proxy.py
similarity index 97%
rename from src/fastmcp/server/auth/proxy.py
rename to src/fastmcp/server/auth/oauth_proxy.py
index 2c4a7de32..862482e2a 100644
--- a/src/fastmcp/server/auth/proxy.py
+++ b/src/fastmcp/server/auth/oauth_proxy.py
@@ -225,7 +225,7 @@ class OAuthProxy(OAuthProvider):
token_verifier: TokenVerifier,
# FastMCP server configuration
base_url: AnyHttpUrl | str,
- redirect_path: str = "/oauth/callback",
+ redirect_path: str = "/auth/callback",
issuer_url: AnyHttpUrl | str | None = None,
service_documentation_url: AnyHttpUrl | str | None = None,
resource_server_url: AnyHttpUrl | str | None = None,
@@ -240,7 +240,7 @@ class OAuthProxy(OAuthProvider):
upstream_revocation_endpoint: Optional upstream revocation endpoint
token_verifier: Token verifier for validating access tokens
base_url: Public URL of this FastMCP server
- redirect_path: Redirect path configured in upstream OAuth app (defaults to "/oauth/callback")
+ redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
issuer_url: Issuer URL for OAuth metadata (defaults to base_url)
service_documentation_url: Optional service documentation URL
resource_server_url: Resource server URL (defaults to base_url)
@@ -321,7 +321,7 @@ class OAuthProxy(OAuthProvider):
# Use the provided token validator
self._token_validator = token_verifier
- logger.info(
+ logger.debug(
"Initialized OAuth proxy provider with upstream server %s",
self._upstream_authorization_endpoint,
)
@@ -408,7 +408,7 @@ class OAuthProxy(OAuthProvider):
# Store the ProxyDCRClient using the upstream ID
self._clients[upstream_id] = proxy_client
- logger.info(
+ logger.debug(
"Registered client %s with %d redirect URIs",
upstream_id,
len(proxy_client.redirect_uris),
@@ -455,12 +455,6 @@ class OAuthProxy(OAuthProvider):
# Add scopes - use client scopes or fallback to required scopes
scopes_to_use = params.scopes or self.required_scopes or []
- # Google requires at least some scope parameter, so provide a minimal one if none specified
- if (
- not scopes_to_use
- and "google" in self._upstream_authorization_endpoint.lower()
- ):
- scopes_to_use = ["openid"] # Minimal scope for Google
if scopes_to_use:
query_params["scope"] = " ".join(scopes_to_use)
@@ -470,12 +464,11 @@ class OAuthProxy(OAuthProvider):
f"{self._upstream_authorization_endpoint}?{urlencode(query_params)}"
)
- logger.info(
+ logger.debug(
"Starting OAuth transaction %s for client %s, redirecting to IdP",
txn_id,
client.client_id,
)
-
return upstream_url
# -------------------------------------------------------------------------
@@ -580,7 +573,7 @@ class OAuthProxy(OAuthProvider):
self._access_to_refresh[access_token_value] = refresh_token_value
self._refresh_to_access[refresh_token_value] = access_token_value
- logger.info(
+ logger.debug(
"Successfully exchanged client code for stored IdP tokens (client: %s)",
client.client_id,
)
@@ -624,7 +617,7 @@ class OAuthProxy(OAuthProvider):
scope=" ".join(scopes) if scopes else None,
)
- logger.info(
+ logger.debug(
"Successfully refreshed access token via authlib (client: %s)",
client.client_id,
)
@@ -727,13 +720,13 @@ class OAuthProxy(OAuthProvider):
self._upstream_client_secret.get_secret_value(),
),
)
- logger.info("Successfully revoked token with upstream server")
+ logger.debug("Successfully revoked token with upstream server")
except Exception as e:
logger.warning("Failed to revoke token with upstream server: %s", e)
else:
logger.debug("No upstream revocation endpoint configured")
- logger.info("Token revoked successfully")
+ logger.debug("Token revoked successfully")
# -------------------------------------------------------------------------
# Custom Route Handling
@@ -786,7 +779,7 @@ class OAuthProxy(OAuthProvider):
if "access_token" in token_data:
self._store_tokens_from_response(token_data)
- logger.info(
+ logger.debug(
"Successfully proxied authorization code exchange via authlib"
)
@@ -811,7 +804,7 @@ class OAuthProxy(OAuthProvider):
else None,
)
- logger.info(
+ logger.debug(
"Successfully proxied refresh token exchange via authlib"
)
@@ -895,7 +888,7 @@ class OAuthProxy(OAuthProvider):
custom_routes = []
token_route_found = False
- logger.info(
+ logger.debug(
f"get_routes called - configuring OAuth routes in {len(routes)} routes"
)
@@ -914,11 +907,6 @@ class OAuthProxy(OAuthProvider):
and "POST" in route.methods
):
token_route_found = True
- logger.info("✅ KEEPING standard token endpoint for DCR-compliant flow")
-
- if not token_route_found:
- logger.warning("⚠️ No /token POST route found!")
- # This shouldn't happen with standard OAuth provider
# Add OAuth callback endpoint for forwarding to client callbacks
custom_routes.append(
@@ -929,7 +917,7 @@ class OAuthProxy(OAuthProvider):
)
)
- logger.info(
+ logger.debug(
f"✅ OAuth routes configured: token_endpoint={token_route_found}, total routes={len(custom_routes)} (includes OAuth callback)"
)
return custom_routes
@@ -1001,7 +989,7 @@ class OAuthProxy(OAuthProvider):
redirect_uri=idp_redirect_uri,
)
- logger.info(
+ logger.debug(
f"Successfully exchanged IdP code for tokens (transaction: {txn_id})"
)
diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py
new file mode 100644
index 000000000..2bb0b924c
--- /dev/null
+++ b/src/fastmcp/server/auth/providers/azure.py
@@ -0,0 +1,250 @@
+"""Azure (Microsoft Entra) OAuth provider for FastMCP.
+
+This provider implements Azure/Microsoft Entra ID OAuth authentication
+using the OAuth Proxy pattern for non-DCR OAuth flows.
+"""
+
+from __future__ import annotations
+
+import httpx
+from pydantic import SecretStr
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+from fastmcp.server.auth import AccessToken, TokenVerifier
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
+from fastmcp.server.auth.registry import register_provider
+from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.types import NotSet, NotSetT
+
+logger = get_logger(__name__)
+
+
+class AzureProviderSettings(BaseSettings):
+ """Settings for Azure OAuth provider."""
+
+ model_config = SettingsConfigDict(
+ env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
+ env_file=".env",
+ extra="ignore",
+ )
+
+ client_id: str | None = None
+ client_secret: SecretStr | None = None
+ tenant_id: str | None = None
+ base_url: str | None = None
+ redirect_path: str | None = None
+ required_scopes: list[str] | None = None
+ timeout_seconds: int | None = None
+
+
+class AzureTokenVerifier(TokenVerifier):
+ """Token verifier for Azure OAuth tokens.
+
+ Azure tokens are JWTs, but we verify them by calling the Microsoft Graph API
+ to get user information and validate the token.
+ """
+
+ def __init__(
+ self,
+ *,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ ):
+ """Initialize the Azure token verifier.
+
+ Args:
+ required_scopes: Required OAuth scopes
+ timeout_seconds: HTTP request timeout
+ """
+ super().__init__(required_scopes=required_scopes)
+ self.timeout_seconds = timeout_seconds
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify Azure OAuth token by calling Microsoft Graph API."""
+ try:
+ async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
+ # Use Microsoft Graph API to validate token and get user info
+ response = await client.get(
+ "https://graph.microsoft.com/v1.0/me",
+ headers={
+ "Authorization": f"Bearer {token}",
+ "User-Agent": "FastMCP-Azure-OAuth",
+ },
+ )
+
+ if response.status_code != 200:
+ logger.debug(
+ "Azure token verification failed: %d - %s",
+ response.status_code,
+ response.text[:200],
+ )
+ return None
+
+ user_data = response.json()
+
+ # Create AccessToken with Azure user info
+ return AccessToken(
+ token=token,
+ client_id=str(user_data.get("id", "unknown")),
+ scopes=self.required_scopes or [],
+ expires_at=None,
+ claims={
+ "sub": user_data.get("id"),
+ "email": user_data.get("mail")
+ or user_data.get("userPrincipalName"),
+ "name": user_data.get("displayName"),
+ "given_name": user_data.get("givenName"),
+ "family_name": user_data.get("surname"),
+ "job_title": user_data.get("jobTitle"),
+ "office_location": user_data.get("officeLocation"),
+ },
+ )
+
+ except httpx.RequestError as e:
+ logger.debug("Failed to verify Azure token: %s", e)
+ return None
+ except Exception as e:
+ logger.debug("Azure token verification error: %s", e)
+ return None
+
+
+@register_provider("AZURE")
+class AzureProvider(OAuthProxy):
+ """Azure (Microsoft Entra) OAuth provider for FastMCP.
+
+ This provider implements Azure/Microsoft Entra ID authentication using the
+ OAuth Proxy pattern. It supports both organizational accounts and personal
+ Microsoft accounts depending on the tenant configuration.
+
+ Features:
+ - Transparent OAuth proxy to Azure/Microsoft identity platform
+ - Automatic token validation via Microsoft Graph API
+ - User information extraction
+ - Support for different tenant configurations (common, organizations, consumers)
+
+ Setup Requirements:
+ 1. Register an application in Azure Portal (portal.azure.com)
+ 2. Configure redirect URI as: http://localhost:8000/auth/callback
+ 3. Note your Application (client) ID and create a client secret
+ 4. Optionally note your Directory (tenant) ID for single-tenant apps
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.auth.providers.azure import AzureProvider
+
+ auth = AzureProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id", # Required: your Azure tenant ID from Azure Portal
+ base_url="http://localhost:8000"
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str | NotSetT = NotSet,
+ client_secret: str | NotSetT = NotSet,
+ tenant_id: str | NotSetT = NotSet,
+ base_url: str | NotSetT = NotSet,
+ redirect_path: str | NotSetT = NotSet,
+ required_scopes: list[str] | None | NotSetT = NotSet,
+ timeout_seconds: int | NotSetT = NotSet,
+ ):
+ """Initialize Azure OAuth provider.
+
+ Args:
+ client_id: Azure application (client) ID
+ client_secret: Azure client secret
+ tenant_id: Azure tenant ID (your specific tenant ID, "organizations", or "consumers")
+ base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
+ required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
+ timeout_seconds: HTTP request timeout for Azure API calls
+ """
+ settings = AzureProviderSettings.model_validate(
+ {
+ k: v
+ for k, v in {
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "tenant_id": tenant_id,
+ "base_url": base_url,
+ "redirect_path": redirect_path,
+ "required_scopes": required_scopes,
+ "timeout_seconds": timeout_seconds,
+ }.items()
+ if v is not NotSet
+ }
+ )
+
+ # Validate required settings
+ if not settings.client_id:
+ raise ValueError(
+ "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
+ )
+ if not settings.client_secret:
+ raise ValueError(
+ "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
+ )
+
+ # Validate tenant_id is provided
+ if not settings.tenant_id:
+ raise ValueError(
+ "tenant_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. "
+ "Use your Azure tenant ID (found in Azure Portal), 'organizations', or 'consumers'"
+ )
+
+ # Apply defaults
+ tenant_id_final = settings.tenant_id
+ base_url_final = settings.base_url or "http://localhost:8000"
+ redirect_path_final = settings.redirect_path or "/auth/callback"
+ timeout_seconds_final = settings.timeout_seconds or 10
+ # Default scopes for Azure - User.Read gives us access to user info via Graph API
+ scopes_final = settings.required_scopes or [
+ "User.Read",
+ "email",
+ "openid",
+ "profile",
+ ]
+
+ # Extract secret string from SecretStr
+ client_secret_str = (
+ settings.client_secret.get_secret_value() if settings.client_secret else ""
+ )
+
+ # Create Azure token verifier
+ token_verifier = AzureTokenVerifier(
+ required_scopes=scopes_final,
+ timeout_seconds=timeout_seconds_final,
+ )
+
+ # Build Azure OAuth endpoints with tenant
+ authorization_endpoint = (
+ f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
+ )
+ token_endpoint = (
+ f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/token"
+ )
+
+ # Initialize OAuth proxy with Azure endpoints
+ super().__init__(
+ upstream_authorization_endpoint=authorization_endpoint,
+ upstream_token_endpoint=token_endpoint,
+ upstream_client_id=settings.client_id,
+ upstream_client_secret=client_secret_str,
+ token_verifier=token_verifier,
+ base_url=base_url_final,
+ redirect_path=redirect_path_final,
+ issuer_url=base_url_final,
+ )
+
+ logger.info(
+ "Initialized Azure OAuth provider for client %s with tenant %s",
+ settings.client_id,
+ tenant_id_final,
+ )
diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py
index e2a27ecae..cddf83fdf 100644
--- a/src/fastmcp/server/auth/providers/github.py
+++ b/src/fastmcp/server/auth/providers/github.py
@@ -27,7 +27,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
-from fastmcp.server.auth.proxy import OAuthProxy
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.registry import register_provider
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -202,7 +202,7 @@ class GitHubProvider(OAuthProxy):
client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
client_secret: GitHub OAuth app client secret
base_url: Public URL of your FastMCP server (for OAuth callbacks)
- redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/oauth/callback")
+ redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
required_scopes: Required GitHub scopes (defaults to ["user"])
timeout_seconds: HTTP request timeout for GitHub API calls
"""
@@ -233,7 +233,7 @@ class GitHubProvider(OAuthProxy):
# Apply defaults
base_url_final = settings.base_url or "http://localhost:8000"
- redirect_path_final = settings.redirect_path or "/oauth/callback"
+ redirect_path_final = settings.redirect_path or "/auth/callback"
timeout_seconds_final = settings.timeout_seconds or 10
required_scopes_final = settings.required_scopes or ["user"]
diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py
index 1c3c46e47..48f61bf8a 100644
--- a/src/fastmcp/server/auth/providers/google.py
+++ b/src/fastmcp/server/auth/providers/google.py
@@ -29,7 +29,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
-from fastmcp.server.auth.proxy import OAuthProxy
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.registry import register_provider
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -218,9 +218,9 @@ class GoogleProvider(OAuthProxy):
client_id: Google OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
client_secret: Google OAuth client secret (e.g., "GOCSPX-abc123...")
base_url: Public URL of your FastMCP server (for OAuth callbacks)
- redirect_path: Redirect path configured in Google OAuth app (defaults to "/oauth/callback")
- required_scopes: Required Google scopes (defaults to []). Common scopes include:
- - "openid" for OpenID Connect
+ redirect_path: Redirect path configured in Google OAuth app (defaults to "/auth/callback")
+ required_scopes: Required Google scopes (defaults to ["openid"]). Common scopes include:
+ - "openid" for OpenID Connect (default)
- "https://www.googleapis.com/auth/userinfo.email" for email access
- "https://www.googleapis.com/auth/userinfo.profile" for profile info
timeout_seconds: HTTP request timeout for Google API calls
@@ -252,9 +252,10 @@ class GoogleProvider(OAuthProxy):
# Apply defaults
base_url_final = settings.base_url or "http://localhost:8000"
- redirect_path_final = settings.redirect_path or "/oauth/callback"
+ redirect_path_final = settings.redirect_path or "/auth/callback"
timeout_seconds_final = settings.timeout_seconds or 10
- required_scopes_final = settings.required_scopes or []
+ # Google requires at least one scope - openid is the minimal OIDC scope
+ required_scopes_final = settings.required_scopes or ["openid"]
# Create Google token verifier
token_verifier = GoogleTokenVerifier(
diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py
index 21ad5f60c..a1d77fd08 100644
--- a/src/fastmcp/server/auth/providers/workos.py
+++ b/src/fastmcp/server/auth/providers/workos.py
@@ -1,12 +1,23 @@
+"""WorkOS authentication providers for FastMCP.
+
+This module provides two WorkOS authentication strategies:
+
+1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)
+2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit
+
+Choose based on your WorkOS setup and authentication requirements.
+"""
+
from __future__ import annotations
import httpx
-from pydantic import AnyHttpUrl
+from pydantic import AnyHttpUrl, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
from starlette.responses import JSONResponse
from starlette.routing import Route
-from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.server.auth.registry import register_provider
from fastmcp.utilities.logging import get_logger
@@ -15,6 +26,224 @@ from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
+class WorkOSProviderSettings(BaseSettings):
+ """Settings for WorkOS OAuth provider."""
+
+ model_config = SettingsConfigDict(
+ env_prefix="FASTMCP_SERVER_AUTH_WORKOS_",
+ env_file=".env",
+ extra="ignore",
+ )
+
+ client_id: str | None = None
+ client_secret: SecretStr | None = None
+ authkit_domain: str | None = None # e.g., "https://your-app.authkit.app"
+ base_url: AnyHttpUrl | str | None = None
+ redirect_path: str | None = None
+ required_scopes: list[str] | None = None
+ timeout_seconds: int | None = None
+
+
+class WorkOSTokenVerifier(TokenVerifier):
+ """Token verifier for WorkOS OAuth tokens.
+
+ WorkOS AuthKit tokens are opaque, so we verify them by calling
+ the /oauth2/userinfo endpoint to check validity and get user info.
+ """
+
+ def __init__(
+ self,
+ *,
+ authkit_domain: str,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ ):
+ """Initialize the WorkOS token verifier.
+
+ Args:
+ authkit_domain: WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
+ required_scopes: Required OAuth scopes
+ timeout_seconds: HTTP request timeout
+ """
+ super().__init__(required_scopes=required_scopes)
+ self.authkit_domain = authkit_domain.rstrip("/")
+ self.timeout_seconds = timeout_seconds
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify WorkOS OAuth token by calling userinfo endpoint."""
+ try:
+ async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
+ # Use WorkOS AuthKit userinfo endpoint to validate token
+ response = await client.get(
+ f"{self.authkit_domain}/oauth2/userinfo",
+ headers={
+ "Authorization": f"Bearer {token}",
+ "User-Agent": "FastMCP-WorkOS-OAuth",
+ },
+ )
+
+ if response.status_code != 200:
+ logger.debug(
+ "WorkOS token verification failed: %d - %s",
+ response.status_code,
+ response.text[:200],
+ )
+ return None
+
+ user_data = response.json()
+
+ # Create AccessToken with WorkOS user info
+ return AccessToken(
+ token=token,
+ client_id=str(user_data.get("sub", "unknown")),
+ scopes=self.required_scopes or [],
+ expires_at=None, # Will be set from token introspection if needed
+ claims={
+ "sub": user_data.get("sub"),
+ "email": user_data.get("email"),
+ "email_verified": user_data.get("email_verified"),
+ "name": user_data.get("name"),
+ "given_name": user_data.get("given_name"),
+ "family_name": user_data.get("family_name"),
+ },
+ )
+
+ except httpx.RequestError as e:
+ logger.debug("Failed to verify WorkOS token: %s", e)
+ return None
+ except Exception as e:
+ logger.debug("WorkOS token verification error: %s", e)
+ return None
+
+
+@register_provider("WORKOS")
+class WorkOSProvider(OAuthProxy):
+ """Complete WorkOS OAuth provider for FastMCP.
+
+ This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.
+ It provides OAuth2 authentication for users through WorkOS Connect applications.
+
+ Features:
+ - Transparent OAuth proxy to WorkOS AuthKit
+ - Automatic token validation via userinfo endpoint
+ - User information extraction from ID tokens
+ - Support for standard OAuth scopes (openid, profile, email)
+
+ Setup Requirements:
+ 1. Create a WorkOS Connect application in your dashboard
+ 2. Note your AuthKit domain (e.g., "https://your-app.authkit.app")
+ 3. Configure redirect URI as: http://localhost:8000/auth/callback
+ 4. Note your Client ID and Client Secret
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.auth.providers.workos import WorkOSProvider
+
+ auth = WorkOSProvider(
+ client_id="client_123",
+ client_secret="sk_test_456",
+ authkit_domain="https://your-app.authkit.app",
+ base_url="http://localhost:8000"
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str | NotSetT = NotSet,
+ client_secret: str | NotSetT = NotSet,
+ authkit_domain: str | NotSetT = NotSet,
+ base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ redirect_path: str | NotSetT = NotSet,
+ required_scopes: list[str] | None | NotSetT = NotSet,
+ timeout_seconds: int | NotSetT = NotSet,
+ ):
+ """Initialize WorkOS OAuth provider.
+
+ Args:
+ client_id: WorkOS client ID
+ client_secret: WorkOS client secret
+ authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
+ base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
+ required_scopes: Required OAuth scopes (no default)
+ timeout_seconds: HTTP request timeout for WorkOS API calls
+ """
+ settings = WorkOSProviderSettings.model_validate(
+ {
+ k: v
+ for k, v in {
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "authkit_domain": authkit_domain,
+ "base_url": base_url,
+ "redirect_path": redirect_path,
+ "required_scopes": required_scopes,
+ "timeout_seconds": timeout_seconds,
+ }.items()
+ if v is not NotSet
+ }
+ )
+
+ # Validate required settings
+ if not settings.client_id:
+ raise ValueError(
+ "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID"
+ )
+ if not settings.client_secret:
+ raise ValueError(
+ "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET"
+ )
+ if not settings.authkit_domain:
+ raise ValueError(
+ "authkit_domain is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN"
+ )
+
+ # Apply defaults and ensure authkit_domain is a full URL
+ authkit_domain_str = settings.authkit_domain
+ if not authkit_domain_str.startswith(("http://", "https://")):
+ authkit_domain_str = f"https://{authkit_domain_str}"
+ authkit_domain_final = authkit_domain_str.rstrip("/")
+ base_url_final = settings.base_url or "http://localhost:8000"
+ redirect_path_final = settings.redirect_path or "/auth/callback"
+ timeout_seconds_final = settings.timeout_seconds or 10
+ scopes_final = settings.required_scopes or []
+
+ # Extract secret string from SecretStr
+ client_secret_str = (
+ settings.client_secret.get_secret_value() if settings.client_secret else ""
+ )
+
+ # Create WorkOS token verifier
+ token_verifier = WorkOSTokenVerifier(
+ authkit_domain=authkit_domain_final,
+ required_scopes=scopes_final,
+ timeout_seconds=timeout_seconds_final,
+ )
+
+ # Initialize OAuth proxy with WorkOS AuthKit endpoints
+ super().__init__(
+ upstream_authorization_endpoint=f"{authkit_domain_final}/oauth2/authorize",
+ upstream_token_endpoint=f"{authkit_domain_final}/oauth2/token",
+ upstream_client_id=settings.client_id,
+ upstream_client_secret=client_secret_str,
+ token_verifier=token_verifier,
+ base_url=base_url_final,
+ redirect_path=redirect_path_final,
+ issuer_url=base_url_final,
+ )
+
+ logger.info(
+ "Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
+ settings.client_id,
+ authkit_domain_final,
+ )
+
+
class AuthKitProviderSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 4d2866db0..fc2ba562e 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -463,7 +463,7 @@ class FastMCP(Generic[LifespanResultT]):
Request and returns a Response.
Args:
- path: URL path for the route (e.g., "/oauth/callback")
+ path: URL path for the route (e.g., "/auth/callback")
methods: List of HTTP methods to support (e.g., ["GET", "POST"])
name: Optional name for the route (to reference this route with
Starlette's reverse URL lookup feature)
diff --git a/tests/integration_tests/auth/test_github_provider_integration.py b/tests/integration_tests/auth/test_github_provider_integration.py
index 909900119..1fbb14b8c 100644
--- a/tests/integration_tests/auth/test_github_provider_integration.py
+++ b/tests/integration_tests/auth/test_github_provider_integration.py
@@ -4,7 +4,7 @@ Tests the complete GitHub OAuth flow using HeadlessOAuth to bypass browser inter
This test requires a GitHub OAuth app to be created at https://github.com/settings/developers
with the following configuration:
-- Redirect URL: http://127.0.0.1:9100/oauth/callback
+- Redirect URL: http://127.0.0.1:9100/auth/callback
- Client ID and Client Secret should be set as environment variables:
- FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID
- FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET
@@ -267,7 +267,7 @@ async def test_github_oauth_authorization_redirect(github_server: str):
# The redirect_uri should be our proxy's callback, not the client's
proxy_callback = github_params["redirect_uri"][0]
assert proxy_callback.startswith(base_url)
- assert proxy_callback.endswith("/oauth/callback")
+ assert proxy_callback.endswith("/auth/callback")
async def test_github_oauth_server_metadata(github_server: str):
diff --git a/tests/server/auth/__init__.py b/tests/server/auth/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/server/auth/providers/__init__.py b/tests/server/auth/providers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py
new file mode 100644
index 000000000..b47c0e837
--- /dev/null
+++ b/tests/server/auth/providers/test_azure.py
@@ -0,0 +1,153 @@
+"""Tests for Azure (Microsoft Entra) OAuth provider."""
+
+import os
+from unittest.mock import patch
+from urllib.parse import urlparse
+
+import pytest
+
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+
+class TestAzureProvider:
+ """Test Azure OAuth provider functionality."""
+
+ def test_init_with_explicit_params(self):
+ """Test AzureProvider initialization with explicit parameters."""
+ provider = AzureProvider(
+ client_id="12345678-1234-1234-1234-123456789012",
+ client_secret="azure_secret_123",
+ tenant_id="87654321-4321-4321-4321-210987654321",
+ base_url="https://myserver.com",
+ required_scopes=["User.Read", "Mail.Read"],
+ )
+
+ assert provider._upstream_client_id == "12345678-1234-1234-1234-123456789012"
+ assert provider._upstream_client_secret.get_secret_value() == "azure_secret_123"
+ assert str(provider.base_url) == "https://myserver.com/"
+ # Check tenant is in the endpoints
+ parsed_auth = urlparse(provider._upstream_authorization_endpoint)
+ assert "87654321-4321-4321-4321-210987654321" in parsed_auth.path
+ parsed_token = urlparse(provider._upstream_token_endpoint)
+ assert "87654321-4321-4321-4321-210987654321" in parsed_token.path
+
+ def test_init_with_env_vars(self):
+ """Test AzureProvider initialization from environment variables."""
+ with patch.dict(
+ os.environ,
+ {
+ "FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID": "env-client-id",
+ "FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET": "env-secret",
+ "FASTMCP_SERVER_AUTH_AZURE_TENANT_ID": "env-tenant-id",
+ "FASTMCP_SERVER_AUTH_AZURE_BASE_URL": "https://envserver.com",
+ "FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES": '["User.Read", "Calendar.Read"]',
+ },
+ ):
+ provider = AzureProvider()
+
+ assert provider._upstream_client_id == "env-client-id"
+ assert provider._upstream_client_secret.get_secret_value() == "env-secret"
+ assert str(provider.base_url) == "https://envserver.com/"
+ # Check tenant is in the endpoints
+ parsed_auth = urlparse(provider._upstream_authorization_endpoint)
+ assert "env-tenant-id" in parsed_auth.path
+ parsed_token = urlparse(provider._upstream_token_endpoint)
+ assert "env-tenant-id" in parsed_token.path
+
+ def test_init_missing_client_id_raises_error(self):
+ """Test that missing client_id raises ValueError."""
+ with pytest.raises(ValueError, match="client_id is required"):
+ AzureProvider(
+ client_secret="test_secret",
+ tenant_id="test-tenant",
+ )
+
+ def test_init_missing_client_secret_raises_error(self):
+ """Test that missing client_secret raises ValueError."""
+ with pytest.raises(ValueError, match="client_secret is required"):
+ AzureProvider(
+ client_id="test_client",
+ tenant_id="test-tenant",
+ )
+
+ def test_init_missing_tenant_id_raises_error(self):
+ """Test that missing tenant_id raises ValueError."""
+ with pytest.raises(ValueError, match="tenant_id is required"):
+ AzureProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ )
+
+ def test_init_defaults(self):
+ """Test that default values are applied correctly."""
+ provider = AzureProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ tenant_id="test-tenant",
+ )
+
+ # Check defaults
+ assert str(provider.base_url) == "http://localhost:8000/"
+ assert provider._redirect_path == "/auth/callback"
+ # Azure provider defaults are set but we can't easily verify them without accessing internals
+
+ def test_oauth_endpoints_configured_correctly(self):
+ """Test that OAuth endpoints are configured correctly."""
+ provider = AzureProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ tenant_id="my-tenant-id",
+ base_url="https://myserver.com",
+ )
+
+ # Check that endpoints use the correct Azure OAuth2 v2.0 endpoints with tenant
+ assert (
+ provider._upstream_authorization_endpoint
+ == "https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/authorize"
+ )
+ assert (
+ provider._upstream_token_endpoint
+ == "https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/token"
+ )
+ assert (
+ provider._upstream_revocation_endpoint is None
+ ) # Azure doesn't support revocation
+
+ def test_special_tenant_values(self):
+ """Test that special tenant values are accepted."""
+ # Test with "organizations"
+ provider1 = AzureProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ tenant_id="organizations",
+ )
+ parsed = urlparse(provider1._upstream_authorization_endpoint)
+ assert "/organizations/" in parsed.path
+
+ # Test with "consumers"
+ provider2 = AzureProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ tenant_id="consumers",
+ )
+ parsed = urlparse(provider2._upstream_authorization_endpoint)
+ assert "/consumers/" in parsed.path
+
+ def test_azure_specific_scopes(self):
+ """Test handling of Azure-specific scope formats."""
+ # Just test that the provider accepts Azure-specific scopes without error
+ provider = AzureProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ tenant_id="test-tenant",
+ required_scopes=[
+ "User.Read",
+ "Mail.Read",
+ "Calendar.ReadWrite",
+ "openid",
+ "profile",
+ ],
+ )
+
+ # Provider should initialize successfully with these scopes
+ assert provider is not None
diff --git a/tests/server/auth/providers/test_github.py b/tests/server/auth/providers/test_github.py
index 31505679a..7c3725f31 100644
--- a/tests/server/auth/providers/test_github.py
+++ b/tests/server/auth/providers/test_github.py
@@ -120,13 +120,17 @@ class TestGitHubProvider:
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
- with pytest.raises(ValueError, match="client_id is required"):
- GitHubProvider(client_secret="test_secret")
+ # Clear environment variables to test proper error handling
+ with patch.dict(os.environ, {}, clear=True):
+ with pytest.raises(ValueError, match="client_id is required"):
+ GitHubProvider(client_secret="test_secret")
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
- with pytest.raises(ValueError, match="client_secret is required"):
- GitHubProvider(client_id="test_client")
+ # Clear environment variables to test proper error handling
+ with patch.dict(os.environ, {}, clear=True):
+ with pytest.raises(ValueError, match="client_secret is required"):
+ GitHubProvider(client_id="test_client")
def test_init_defaults(self):
"""Test that default values are applied correctly."""
@@ -137,7 +141,7 @@ class TestGitHubProvider:
# Check defaults
assert str(provider.base_url) == "http://localhost:8000/"
- assert provider._redirect_path == "/oauth/callback"
+ assert provider._redirect_path == "/auth/callback"
# The required_scopes should be passed to the token verifier
assert provider._token_validator.required_scopes == ["user"]
diff --git a/tests/server/auth/providers/test_google.py b/tests/server/auth/providers/test_google.py
new file mode 100644
index 000000000..3f68eb152
--- /dev/null
+++ b/tests/server/auth/providers/test_google.py
@@ -0,0 +1,105 @@
+"""Tests for Google OAuth provider."""
+
+import os
+from unittest.mock import patch
+
+import pytest
+
+from fastmcp.server.auth.providers.google import GoogleProvider
+
+
+class TestGoogleProvider:
+ """Test Google OAuth provider functionality."""
+
+ def test_init_with_explicit_params(self):
+ """Test GoogleProvider initialization with explicit parameters."""
+ provider = GoogleProvider(
+ client_id="123456789.apps.googleusercontent.com",
+ client_secret="GOCSPX-test123",
+ base_url="https://myserver.com",
+ required_scopes=["openid", "email", "profile"],
+ )
+
+ assert provider._upstream_client_id == "123456789.apps.googleusercontent.com"
+ assert provider._upstream_client_secret.get_secret_value() == "GOCSPX-test123"
+ assert str(provider.base_url) == "https://myserver.com/"
+
+ def test_init_with_env_vars(self):
+ """Test GoogleProvider initialization from environment variables."""
+ with patch.dict(
+ os.environ,
+ {
+ "FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID": "env123.apps.googleusercontent.com",
+ "FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET": "GOCSPX-env456",
+ "FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL": "https://envserver.com",
+ "FASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES": '["openid", "https://www.googleapis.com/auth/userinfo.email"]',
+ },
+ ):
+ provider = GoogleProvider()
+
+ assert provider._upstream_client_id == "env123.apps.googleusercontent.com"
+ assert (
+ provider._upstream_client_secret.get_secret_value() == "GOCSPX-env456"
+ )
+ assert str(provider.base_url) == "https://envserver.com/"
+
+ def test_init_missing_client_id_raises_error(self):
+ """Test that missing client_id raises ValueError."""
+ # Clear environment variables to test proper error handling
+ with patch.dict(os.environ, {}, clear=True):
+ with pytest.raises(ValueError, match="client_id is required"):
+ GoogleProvider(client_secret="GOCSPX-test123")
+
+ def test_init_missing_client_secret_raises_error(self):
+ """Test that missing client_secret raises ValueError."""
+ # Clear environment variables to test proper error handling
+ with patch.dict(os.environ, {}, clear=True):
+ with pytest.raises(ValueError, match="client_secret is required"):
+ GoogleProvider(client_id="123456789.apps.googleusercontent.com")
+
+ def test_init_defaults(self):
+ """Test that default values are applied correctly."""
+ provider = GoogleProvider(
+ client_id="123456789.apps.googleusercontent.com",
+ client_secret="GOCSPX-test123",
+ )
+
+ # Check defaults
+ assert str(provider.base_url) == "http://localhost:8000/"
+ assert provider._redirect_path == "/auth/callback"
+ # Google provider has ["openid"] as default but we can't easily verify without accessing internals
+
+ def test_oauth_endpoints_configured_correctly(self):
+ """Test that OAuth endpoints are configured correctly."""
+ provider = GoogleProvider(
+ client_id="123456789.apps.googleusercontent.com",
+ client_secret="GOCSPX-test123",
+ base_url="https://myserver.com",
+ )
+
+ # Check that endpoints use Google's OAuth2 endpoints
+ assert (
+ provider._upstream_authorization_endpoint
+ == "https://accounts.google.com/o/oauth2/v2/auth"
+ )
+ assert (
+ provider._upstream_token_endpoint == "https://oauth2.googleapis.com/token"
+ )
+ # Google provider doesn't currently set a revocation endpoint
+ assert provider._upstream_revocation_endpoint is None
+
+ def test_google_specific_scopes(self):
+ """Test handling of Google-specific scope formats."""
+ # Just test that the provider accepts Google-specific scopes without error
+ provider = GoogleProvider(
+ client_id="123456789.apps.googleusercontent.com",
+ client_secret="GOCSPX-test123",
+ required_scopes=[
+ "openid",
+ "https://www.googleapis.com/auth/userinfo.email",
+ "https://www.googleapis.com/auth/userinfo.profile",
+ ],
+ )
+
+ # Provider should initialize successfully with these scopes
+ assert provider is not None
diff --git a/tests/server/auth/providers/test_workos.py b/tests/server/auth/providers/test_workos.py
new file mode 100644
index 000000000..8f2643bef
--- /dev/null
+++ b/tests/server/auth/providers/test_workos.py
@@ -0,0 +1,141 @@
+"""Tests for WorkOS OAuth provider."""
+
+import os
+from unittest.mock import patch
+from urllib.parse import urlparse
+
+import pytest
+
+from fastmcp.server.auth.providers.workos import WorkOSProvider
+
+
+class TestWorkOSProvider:
+ """Test WorkOS OAuth provider functionality."""
+
+ def test_init_with_explicit_params(self):
+ """Test WorkOSProvider initialization with explicit parameters."""
+ provider = WorkOSProvider(
+ client_id="client_test123",
+ client_secret="secret_test456",
+ authkit_domain="https://test.authkit.app",
+ base_url="https://myserver.com",
+ required_scopes=["openid", "profile"],
+ )
+
+ assert provider._upstream_client_id == "client_test123"
+ assert provider._upstream_client_secret.get_secret_value() == "secret_test456"
+ assert str(provider.base_url) == "https://myserver.com/"
+
+ def test_init_with_env_vars(self):
+ """Test WorkOSProvider initialization from environment variables."""
+ with patch.dict(
+ os.environ,
+ {
+ "FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID": "env_client",
+ "FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET": "env_secret",
+ "FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN": "https://env.authkit.app",
+ "FASTMCP_SERVER_AUTH_WORKOS_BASE_URL": "https://envserver.com",
+ "FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES": '["openid", "email"]',
+ },
+ ):
+ provider = WorkOSProvider()
+
+ assert provider._upstream_client_id == "env_client"
+ assert provider._upstream_client_secret.get_secret_value() == "env_secret"
+ assert str(provider.base_url) == "https://envserver.com/"
+
+ def test_init_missing_client_id_raises_error(self):
+ """Test that missing client_id raises ValueError."""
+ with pytest.raises(ValueError, match="client_id is required"):
+ WorkOSProvider(
+ client_secret="test_secret",
+ authkit_domain="https://test.authkit.app",
+ )
+
+ def test_init_missing_client_secret_raises_error(self):
+ """Test that missing client_secret raises ValueError."""
+ with pytest.raises(ValueError, match="client_secret is required"):
+ WorkOSProvider(
+ client_id="test_client",
+ authkit_domain="https://test.authkit.app",
+ )
+
+ def test_init_missing_authkit_domain_raises_error(self):
+ """Test that missing authkit_domain raises ValueError."""
+ with pytest.raises(ValueError, match="authkit_domain is required"):
+ WorkOSProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ )
+
+ def test_authkit_domain_https_prefix_handling(self):
+ """Test that authkit_domain handles missing https:// prefix."""
+ # Without https:// - should add it
+ provider1 = WorkOSProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ authkit_domain="test.authkit.app",
+ base_url="https://myserver.com",
+ )
+ parsed = urlparse(provider1._upstream_authorization_endpoint)
+ assert parsed.scheme == "https"
+ assert parsed.netloc == "test.authkit.app"
+ assert parsed.path == "/oauth2/authorize"
+
+ # With https:// - should keep it
+ provider2 = WorkOSProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ authkit_domain="https://test.authkit.app",
+ base_url="https://myserver.com",
+ )
+ parsed = urlparse(provider2._upstream_authorization_endpoint)
+ assert parsed.scheme == "https"
+ assert parsed.netloc == "test.authkit.app"
+ assert parsed.path == "/oauth2/authorize"
+
+ # With http:// - should be preserved
+ provider3 = WorkOSProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ authkit_domain="http://localhost:8080",
+ base_url="https://myserver.com",
+ )
+ parsed = urlparse(provider3._upstream_authorization_endpoint)
+ assert parsed.scheme == "http"
+ assert parsed.netloc == "localhost:8080"
+ assert parsed.path == "/oauth2/authorize"
+
+ def test_init_defaults(self):
+ """Test that default values are applied correctly."""
+ provider = WorkOSProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ authkit_domain="https://test.authkit.app",
+ )
+
+ # Check defaults
+ assert str(provider.base_url) == "http://localhost:8000/"
+ assert provider._redirect_path == "/auth/callback"
+ # WorkOS provider has no default scopes but we can't easily verify without accessing internals
+
+ def test_oauth_endpoints_configured_correctly(self):
+ """Test that OAuth endpoints are configured correctly."""
+ provider = WorkOSProvider(
+ client_id="test_client",
+ client_secret="test_secret",
+ authkit_domain="https://test.authkit.app",
+ base_url="https://myserver.com",
+ )
+
+ # Check that endpoints use the authkit domain
+ assert (
+ provider._upstream_authorization_endpoint
+ == "https://test.authkit.app/oauth2/authorize"
+ )
+ assert (
+ provider._upstream_token_endpoint == "https://test.authkit.app/oauth2/token"
+ )
+ assert (
+ provider._upstream_revocation_endpoint is None
+ ) # WorkOS doesn't support revocation
diff --git a/tests/server/auth/test_oauth_proxy.py b/tests/server/auth/test_oauth_proxy.py
index eded58da1..be1b4f0bf 100644
--- a/tests/server/auth/test_oauth_proxy.py
+++ b/tests/server/auth/test_oauth_proxy.py
@@ -10,8 +10,8 @@ from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyUrl
from fastmcp.server.auth.auth import AccessToken
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.server.auth.proxy import OAuthProxy
class TestOAuthProxyComprehensive:
@@ -35,7 +35,7 @@ class TestOAuthProxyComprehensive:
upstream_client_secret="test-client-secret",
token_verifier=jwt_verifier,
base_url="https://myserver.com",
- redirect_path="/oauth/callback",
+ redirect_path="/auth/callback",
)
def test_initialization_with_string_urls(self, jwt_verifier):
@@ -98,9 +98,9 @@ class TestOAuthProxyComprehensive:
upstream_client_secret="secret",
token_verifier=jwt_verifier,
base_url="https://server.com",
- redirect_path="oauth/callback",
+ redirect_path="auth/callback",
)
- assert proxy1._redirect_path == "/oauth/callback"
+ assert proxy1._redirect_path == "/auth/callback"
# With leading slash
proxy2 = OAuthProxy(
@@ -110,9 +110,9 @@ class TestOAuthProxyComprehensive:
upstream_client_secret="secret",
token_verifier=jwt_verifier,
base_url="https://server.com",
- redirect_path="/oauth/callback",
+ redirect_path="/auth/callback",
)
- assert proxy2._redirect_path == "/oauth/callback"
+ assert proxy2._redirect_path == "/auth/callback"
def test_dcr_always_enabled(self, jwt_verifier):
"""Test that DCR is always enabled for OAuth Proxy."""
@@ -158,7 +158,6 @@ class TestOAuthProxyComprehensive:
assert proxy.revocation_options is None
assert proxy._upstream_revocation_endpoint is None
- @pytest.mark.asyncio
async def test_register_client(self, oauth_proxy):
"""Test client registration always uses upstream credentials."""
client_info = OAuthClientInformationFull(
@@ -183,7 +182,6 @@ class TestOAuthProxyComprehensive:
assert stored_client is not None
assert stored_client.client_id == "test-client-id"
- @pytest.mark.asyncio
async def test_register_client_empty_grant_types(self, oauth_proxy):
"""Test client registration adds grant types when empty."""
client_info = OAuthClientInformationFull(
@@ -198,7 +196,6 @@ class TestOAuthProxyComprehensive:
# Should add both authorization_code and refresh_token
assert client_info.grant_types == ["authorization_code", "refresh_token"]
- @pytest.mark.asyncio
async def test_get_client_existing(self, oauth_proxy):
"""Test getting an existing registered client."""
# Register a client first
@@ -214,7 +211,6 @@ class TestOAuthProxyComprehensive:
assert retrieved is not None
assert retrieved.client_id == "test-client-id"
- @pytest.mark.asyncio
async def test_get_client_temporary(self, oauth_proxy):
"""Test getting a temporary client for unregistered client ID."""
# Get a client that hasn't been registered
@@ -236,7 +232,6 @@ class TestOAuthProxyComprehensive:
)
assert str(test_uri) == "http://localhost:55454/callback"
- @pytest.mark.asyncio
async def test_authorize_creates_transaction(self, oauth_proxy):
"""Test that authorize creates a transaction and returns upstream URL."""
client = OAuthClientInformationFull(
@@ -268,7 +263,7 @@ class TestOAuthProxyComprehensive:
# Verify query parameters
assert query_params["response_type"] == ["code"]
assert query_params["client_id"] == ["test-client-id"]
- assert query_params["redirect_uri"] == ["https://myserver.com/oauth/callback"]
+ assert query_params["redirect_uri"] == ["https://myserver.com/auth/callback"]
assert "state" in query_params # This should be the transaction ID
assert query_params["scope"] == ["read write"]
@@ -283,7 +278,6 @@ class TestOAuthProxyComprehensive:
assert transaction["code_challenge_method"] == "S256"
assert transaction["scopes"] == ["read", "write"]
- @pytest.mark.asyncio
async def test_authorize_without_scopes(self, oauth_proxy):
"""Test authorize without scopes uses required scopes from verifier."""
client = OAuthClientInformationFull(
@@ -308,10 +302,9 @@ class TestOAuthProxyComprehensive:
# Should use required_scopes from token_verifier
assert query_params["scope"] == ["read write"]
- @pytest.mark.asyncio
- async def test_authorize_google_minimal_scope(self, jwt_verifier):
- """Test that Google OAuth gets minimal scope when none specified."""
- # Create proxy with Google endpoints
+ async def test_authorize_no_scopes(self, jwt_verifier):
+ """Test that proxy doesn't add scopes when none specified."""
+ # Create proxy - using Google endpoints but proxy shouldn't special-case
proxy = OAuthProxy(
upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
upstream_token_endpoint="https://oauth2.googleapis.com/token",
@@ -332,7 +325,7 @@ class TestOAuthProxyComprehensive:
redirect_uri_provided_explicitly=True,
state="state",
code_challenge="challenge",
- scopes=[], # Empty scopes to test Google fallback
+ scopes=[], # Empty scopes
)
redirect_url = await proxy.authorize(client, params)
@@ -340,10 +333,9 @@ class TestOAuthProxyComprehensive:
parsed = urlparse(redirect_url)
query_params = parse_qs(parsed.query)
- # Should add minimal scope for Google
- assert query_params["scope"] == ["openid"]
+ # Proxy should NOT add any scopes - providers handle their own defaults
+ assert "scope" not in query_params
- @pytest.mark.asyncio
async def test_load_authorization_code_valid(self, oauth_proxy):
"""Test loading a valid authorization code."""
# Store a client code
@@ -373,7 +365,6 @@ class TestOAuthProxyComprehensive:
assert auth_code.code_challenge == "challenge-123"
assert auth_code.scopes == ["read", "write"]
- @pytest.mark.asyncio
async def test_load_authorization_code_expired(self, oauth_proxy):
"""Test loading an expired authorization code returns None."""
code = "expired-code"
@@ -394,7 +385,6 @@ class TestOAuthProxyComprehensive:
# Code should be cleaned up
assert code not in oauth_proxy._client_codes
- @pytest.mark.asyncio
async def test_load_authorization_code_wrong_client(self, oauth_proxy):
"""Test loading authorization code with wrong client ID returns None."""
code = "test-code"
@@ -413,7 +403,6 @@ class TestOAuthProxyComprehensive:
auth_code = await oauth_proxy.load_authorization_code(wrong_client, code)
assert auth_code is None
- @pytest.mark.asyncio
async def test_load_access_token_delegates_to_verifier(
self, oauth_proxy, jwt_verifier
):
@@ -443,7 +432,7 @@ class TestOAuthProxyComprehensive:
# Find the callback route
callback_routes = [
- r for r in routes if hasattr(r, "path") and r.path == "/oauth/callback"
+ r for r in routes if hasattr(r, "path") and r.path == "/auth/callback"
]
assert len(callback_routes) == 1
@@ -464,9 +453,8 @@ class TestOAuthProxyComprehensive:
assert "/.well-known/oauth-authorization-server" in paths
# Plus our custom callback
- assert "/oauth/callback" in paths
+ assert "/auth/callback" in paths
- @pytest.mark.asyncio
async def test_revoke_token_access_token(self, oauth_proxy):
"""Test revoking an access token cleans up local storage."""
# Store tokens
@@ -492,7 +480,6 @@ class TestOAuthProxyComprehensive:
assert access_token not in oauth_proxy._access_to_refresh
assert refresh_token not in oauth_proxy._refresh_to_access
- @pytest.mark.asyncio
async def test_exchange_authorization_code_stores_tokens(self, oauth_proxy):
"""Test that exchange_authorization_code stores tokens locally."""
from mcp.server.auth.provider import AuthorizationCode