WIP auth provider plugins checkpoint

This commit is contained in:
Jeremiah Lowin 2026-05-10 09:13:02 -04:00
commit 835ba07bcd
No known key found for this signature in database
137 changed files with 6031 additions and 5338 deletions

View file

@ -478,7 +478,7 @@ When mounting an OAuth-protected server under a path prefix, declare your URLs u
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
@ -540,7 +540,7 @@ Here's a complete working example showing all the pieces together:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
import uvicorn

View file

@ -97,7 +97,7 @@ async def my_tool(
For Azure/Entra, the new `fastmcp[azure]` extra adds `EntraOBOToken`, which handles the On-Behalf-Of token exchange declaratively:
```python
from fastmcp.server.auth.providers.azure import EntraOBOToken
from fastmcp.server.plugins.auth.azure.provider import EntraOBOToken
@mcp.tool()
async def get_emails(

View file

@ -252,7 +252,7 @@ auth = GitHubProvider()
# After (v3) — pass values explicitly
import os
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],

View file

@ -47,7 +47,7 @@ Create an Application in your Auth0 settings to get the credentials needed for a
</Warning>
<Tip>
If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0Provider.
If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0 plugin.
</Tip>
</Step>
@ -76,23 +76,25 @@ Create an Application in your Auth0 settings to get the credentials needed for a
### Step 2: FastMCP Configuration
Create your FastMCP server using the `Auth0Provider`.
Create your FastMCP server using the `Auth0Auth` plugin.
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.plugins.auth.auth0 import Auth0Auth
# The Auth0Provider utilizes Auth0 OIDC configuration
auth_provider = Auth0Provider(
config_url="https://.../.well-known/openid-configuration", # Your Auth0 configuration URL
client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB", # Your Auth0 application Client ID
client_secret="vPYqbjemq...", # Your Auth0 application Client Secret
audience="https://...", # Your Auth0 API audience
base_url="http://localhost:8000", # Must match your application configuration
# redirect_path="/auth/callback" # Default value, customize if needed
# The Auth0 plugin utilizes Auth0 OIDC configuration
auth_plugin = Auth0Auth(
Auth0Auth.Config(
config_url="https://.../.well-known/openid-configuration", # Your Auth0 configuration URL
client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB", # Your Auth0 application Client ID
client_secret="vPYqbjemq...", # Your Auth0 application Client Secret
audience="https://...", # Your Auth0 API audience
base_url="http://localhost:8000", # Must match your application configuration
# redirect_path="/auth/callback" # Default value, customize if needed
)
)
mcp = FastMCP(name="Auth0 Secured App", auth=auth_provider)
mcp = FastMCP(name="Auth0 Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@ -157,21 +159,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.plugins.auth.auth0 import Auth0Auth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = Auth0Provider(
config_url="https://.../.well-known/openid-configuration",
client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB",
client_secret="vPYqbjemq...",
audience="https://...",
base_url="https://your-production-domain.com",
# Production token management
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = Auth0Auth(
Auth0Auth.Config(
config_url="https://.../.well-known/openid-configuration",
client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB",
client_secret="vPYqbjemq...",
audience="https://...",
base_url="https://your-production-domain.com",
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -181,7 +183,7 @@ auth_provider = Auth0Provider(
)
)
mcp = FastMCP(name="Production Auth0 App", auth=auth_provider)
mcp = FastMCP(name="Production Auth0 App", plugins=[auth_plugin])
```
<Note>

View file

@ -44,20 +44,22 @@ In the WorkOS Dashboard, go to **Connect → Configuration** and configure:
### Step 2: FastMCP Configuration
Create your FastMCP server file and use the `AuthKitProvider` to handle all the OAuth integration automatically:
Create your FastMCP server file and use the `AuthKitAuth` plugin to handle the OAuth integration automatically:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
from fastmcp.server.plugins.auth.authkit import AuthKitAuth
# AuthKitProvider automatically discovers WorkOS endpoints, configures JWT
# AuthKitAuth automatically discovers WorkOS endpoints, configures JWT
# validation, and binds the token audience to this server's resource URL.
auth_provider = AuthKitProvider(
authkit_domain="https://your-project-12345.authkit.app",
base_url="http://127.0.0.1:8000", # Use your actual server URL
auth_plugin = AuthKitAuth(
AuthKitAuth.Config(
authkit_domain="https://your-project-12345.authkit.app",
base_url="http://127.0.0.1:8000", # Use your actual server URL
)
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth_provider)
mcp = FastMCP(name="AuthKit Secured App", plugins=[auth_plugin])
```
When the server starts, it logs the resource URL it is validating against. Paste that URL into your Dashboard's **MCP resource indicators** list.
@ -94,13 +96,15 @@ For production deployments, load sensitive configuration from environment variab
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
from fastmcp.server.plugins.auth.authkit import AuthKitAuth
# Load configuration from environment variables
auth = AuthKitProvider(
authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
auth_plugin = AuthKitAuth(
AuthKitAuth.Config(
authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
)
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth)
mcp = FastMCP(name="AuthKit Secured App", plugins=[auth_plugin])
```

View file

@ -116,24 +116,26 @@ Set up AWS Cognito user pool with an app client to get the credentials needed fo
### Step 2: FastMCP Configuration
Create your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cognito's JWT tokens and user claims automatically:
Create your FastMCP server using the `AWSCognitoAuth` plugin, which handles AWS Cognito's JWT tokens and user claims automatically:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.aws import AWSCognitoProvider
from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
from fastmcp.server.dependencies import get_access_token
# The AWSCognitoProvider handles JWT validation and user claims
auth_provider = AWSCognitoProvider(
user_pool_id="eu-central-1_XXXXXXXXX", # Your AWS Cognito user pool ID
aws_region="eu-central-1", # AWS region (defaults to eu-central-1)
client_id="your-app-client-id", # Your app client ID
client_secret="your-app-client-secret", # Your app client Secret
base_url="http://localhost:8000", # Must match your callback URL
# redirect_path="/auth/callback" # Default value, customize if needed
# The AWSCognitoAuth plugin handles JWT validation and user claims
auth_plugin = AWSCognitoAuth(
AWSCognitoAuth.Config(
user_pool_id="eu-central-1_XXXXXXXXX", # Your AWS Cognito user pool ID
aws_region="eu-central-1", # AWS region (defaults to eu-central-1)
client_id="your-app-client-id", # Your app client ID
client_secret="your-app-client-secret", # Your app client Secret
base_url="http://localhost:8000", # Must match your callback URL
# redirect_path="/auth/callback" # Default value, customize if needed
)
)
mcp = FastMCP(name="AWS Cognito Secured App", auth=auth_provider)
mcp = FastMCP(name="AWS Cognito Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@ -204,21 +206,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.aws import AWSCognitoProvider
from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = AWSCognitoProvider(
user_pool_id="eu-central-1_XXXXXXXXX",
aws_region="eu-central-1",
client_id="your-app-client-id",
client_secret="your-app-client-secret",
base_url="https://your-production-domain.com",
# Production token management
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = AWSCognitoAuth(
AWSCognitoAuth.Config(
user_pool_id="eu-central-1_XXXXXXXXX",
aws_region="eu-central-1",
client_id="your-app-client-id",
client_secret="your-app-client-secret",
base_url="https://your-production-domain.com",
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -228,7 +230,7 @@ auth_provider = AWSCognitoProvider(
)
)
mcp = FastMCP(name="Production AWS Cognito App", auth=auth_provider)
mcp = FastMCP(name="Production AWS Cognito App", plugins=[auth_plugin])
```
<Note>
@ -275,4 +277,4 @@ Perfect for enterprise environments with:
- **Multi-Factor Authentication (MFA)**: Leverage AWS Cognito's built-in MFA
- **User Groups**: Role-based access control through AWS Cognito groups
- **Custom Attributes**: Access custom user attributes defined in your AWS Cognito user pool
- **Compliance**: Meet enterprise security and compliance requirements
- **Compliance**: Meet enterprise security and compliance requirements

View file

@ -46,7 +46,7 @@ Create an App registration in Azure Portal to get the credentials needed for aut
</Warning>
<Tip>
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.
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 Azure plugin.
</Tip>
- **Expose an API**: Configure your Application ID URI and define scopes
@ -74,7 +74,7 @@ Create an App registration in Azure Portal to get the credentials needed for aut
</Warning>
<Note>
In FastMCP's `AzureProvider`, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.
In FastMCP's Azure plugin, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.
</Note>
@ -109,28 +109,30 @@ Create an App registration in Azure Portal to get the credentials needed for aut
### Step 2: FastMCP Configuration
Create your FastMCP server using the `AzureProvider`, which handles Azure's OAuth flow automatically:
Create your FastMCP server using the `AzureAuth` plugin, which handles Azure's OAuth flow automatically:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.plugins.auth.azure import AzureAuth
# 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=["your-scope"], # At least one scope REQUIRED - name of scope from your App
# identifier_uri defaults to api://{client_id}
# identifier_uri="api://your-api-id",
# Optional: request additional upstream scopes in the authorize request
# additional_authorize_scopes=["User.Read", "openid", "email"],
# redirect_path="/auth/callback" # Default value, customize if needed
# base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
# The AzureAuth plugin handles Azure's token format and validation
auth_plugin = AzureAuth(
AzureAuth.Config(
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=["your-scope"], # At least one scope REQUIRED - name of scope from your App
# identifier_uri defaults to api://{client_id}
# identifier_uri="api://your-api-id",
# Optional: request additional upstream scopes in the authorize request
# additional_authorize_scopes=["User.Read", "openid", "email"],
# redirect_path="/auth/callback" # Default value, customize if needed
# base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
)
)
mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
mcp = FastMCP(name="Azure Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@ -139,7 +141,7 @@ async def get_user_info() -> dict:
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
# The AzureProvider stores user data in token claims
# The Azure plugin stores user data in token claims
return {
"azure_id": token.claims.get("sub"),
"email": token.claims.get("email"),
@ -250,21 +252,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.plugins.auth.azure import AzureAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = AzureProvider(
client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149",
client_secret="your-client-secret",
tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5",
base_url="https://your-production-domain.com",
required_scopes=["your-scope"],
# Production token management
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = AzureAuth(
AzureAuth.Config(
client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149",
client_secret="your-client-secret",
tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5",
base_url="https://your-production-domain.com",
required_scopes=["your-scope"],
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -274,7 +276,7 @@ auth_provider = AzureProvider(
)
)
mcp = FastMCP(name="Production Azure App", auth=auth_provider)
mcp = FastMCP(name="Production Azure App", plugins=[auth_plugin])
```
<Note>
@ -287,7 +289,7 @@ For complete details on these parameters, see the [OAuth Proxy documentation](/s
<VersionBadge version="2.15.0" />
For deployments where your server only needs to **validate incoming tokens** — such as Azure Container Apps with Managed Identity — use `AzureJWTVerifier` with `RemoteAuthProvider` instead of the full `AzureProvider`.
For deployments where your server only needs to **validate incoming tokens** — such as Azure Container Apps with Managed Identity — use `AzureJWTVerifier` with `RemoteAuthProvider` instead of the full Azure auth plugin.
This pattern is ideal when:
- Your infrastructure handles authentication (e.g., Managed Identity)
@ -297,7 +299,7 @@ This pattern is ideal when:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.auth.providers.azure import AzureJWTVerifier
from fastmcp.server.plugins.auth.azure.provider import AzureJWTVerifier
from pydantic import AnyHttpUrl
tenant_id = "your-tenant-id"
@ -371,29 +373,31 @@ OBO requires additional configuration in your Azure App registration beyond basi
</Step>
</Steps>
### Configure AzureProvider for OBO
### Configure AzureAuth for OBO
The `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.plugins.auth.azure import AzureAuth
auth_provider = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"], # Your API scope
# Include Graph scopes for OBO
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
"offline_access", # Enables refresh tokens
],
auth_plugin = AzureAuth(
AzureAuth.Config(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"], # Your API scope
# Include Graph scopes for OBO
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
"offline_access", # Enables refresh tokens
],
)
)
mcp = FastMCP(name="Graph-Enabled Server", auth=auth_provider)
mcp = FastMCP(name="Graph-Enabled Server", plugins=[auth_plugin])
```
Scopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.
@ -408,22 +412,25 @@ The `EntraOBOToken` dependency handles the complete OBO flow automatically. Decl
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
from fastmcp.server.plugins.auth.azure import AzureAuth
from fastmcp.server.plugins.auth.azure.provider import EntraOBOToken
import httpx
auth_provider = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"],
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
],
auth_plugin = AzureAuth(
AzureAuth.Config(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"],
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
],
)
)
mcp = FastMCP(name="Email Reader", auth=auth_provider)
mcp = FastMCP(name="Email Reader", plugins=[auth_plugin])
@mcp.tool
async def get_recent_emails(

View file

@ -54,21 +54,23 @@ SERVER_URL=http://localhost:3000 # Your server's base URL
### Step 3: FastMCP Configuration
Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:
Create your FastMCP server file and use the `DescopeAuth` plugin to handle the OAuth integration automatically:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.descope import DescopeProvider
from fastmcp.server.plugins.auth.descope import DescopeAuth
# The DescopeProvider automatically discovers Descope endpoints
# The DescopeAuth plugin automatically discovers Descope endpoints
# and configures JWT token validation
auth_provider = DescopeProvider(
config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
base_url=SERVER_URL, # Your server's public URL
auth_plugin = DescopeAuth(
DescopeAuth.Config(
config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
base_url=SERVER_URL, # Your server's public URL
)
)
# Create FastMCP server with auth
mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider)
# Create FastMCP server with auth plugin
mcp = FastMCP(name="My Descope Protected Server", plugins=[auth_plugin])
```
@ -101,13 +103,15 @@ For production deployments, load configuration from environment variables:
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.descope import DescopeProvider
from fastmcp.server.plugins.auth.descope import DescopeAuth
# Load configuration from environment variables
auth = DescopeProvider(
config_url=os.environ.get("DESCOPE_CONFIG_URL"),
base_url=os.environ.get("BASE_URL", "https://your-server.com")
auth_plugin = DescopeAuth(
DescopeAuth.Config(
config_url=os.environ.get("DESCOPE_CONFIG_URL"),
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
)
)
mcp = FastMCP(name="My Descope Protected Server", auth=auth)
mcp = FastMCP(name="My Descope Protected Server", plugins=[auth_plugin])
```

View file

@ -56,19 +56,21 @@ Create an application in the Discord Developer Portal to get the credentials nee
### Step 2: FastMCP Configuration
Create your FastMCP server using the `DiscordProvider`, which handles Discord's OAuth flow automatically:
Create your FastMCP server using the `DiscordAuth` plugin, which handles Discord's OAuth flow automatically:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.discord import DiscordProvider
from fastmcp.server.plugins.auth.discord import DiscordAuth
auth_provider = DiscordProvider(
client_id="12345", # Your Discord Application Client ID
client_secret="your-client-secret", # Your Discord OAuth Client Secret
base_url="http://localhost:8000", # Must match your OAuth configuration
auth_plugin = DiscordAuth(
DiscordAuth.Config(
client_id="12345", # Your Discord Application Client ID
client_secret="your-client-secret", # Your Discord OAuth Client Secret
base_url="http://localhost:8000", # Must match your OAuth configuration
)
)
mcp = FastMCP(name="Discord Secured App", auth=auth_provider)
mcp = FastMCP(name="Discord Secured App", plugins=[auth_plugin])
@mcp.tool
async def get_user_info() -> dict:
@ -138,11 +140,13 @@ Discord OAuth supports several scopes for accessing different types of user data
To request additional scopes:
```python
auth_provider = DiscordProvider(
client_id="...",
client_secret="...",
base_url="http://localhost:8000",
required_scopes=["identify", "email"],
auth_plugin = DiscordAuth(
DiscordAuth.Config(
client_id="...",
client_secret="...",
base_url="http://localhost:8000",
required_scopes=["identify", "email"],
)
)
```
@ -153,17 +157,18 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.discord import DiscordProvider
from fastmcp.server.plugins.auth.discord import DiscordAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
auth_provider = DiscordProvider(
client_id="12345",
client_secret=os.environ["DISCORD_CLIENT_SECRET"],
base_url="https://your-production-domain.com",
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = DiscordAuth(
DiscordAuth.Config(
client_id="12345",
client_secret=os.environ["DISCORD_CLIENT_SECRET"],
base_url="https://your-production-domain.com",
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -173,7 +178,7 @@ auth_provider = DiscordProvider(
)
)
mcp = FastMCP(name="Production Discord App", auth=auth_provider)
mcp = FastMCP(name="Production Discord App", plugins=[auth_plugin])
```
<Note>

View file

@ -42,7 +42,7 @@ Create an OAuth App in your GitHub settings to get the credentials needed for au
</Warning>
<Tip>
If you want to use a custom callback path (e.g., `/auth/github/callback`), make sure to set the same path in both your GitHub OAuth App settings and the `redirect_path` parameter when configuring the GitHubProvider.
If you want to use a custom callback path (e.g., `/auth/github/callback`), make sure to set the same path in both your GitHub OAuth App settings and the `redirect_path` parameter when configuring the GitHub plugin.
</Tip>
</Step>
@ -60,21 +60,23 @@ Create an OAuth App in your GitHub settings to get the credentials needed for au
### Step 2: FastMCP Configuration
Create your FastMCP server using the `GitHubProvider`, which handles GitHub's OAuth quirks automatically:
Create your FastMCP server using the `GitHubAuth` plugin, which handles GitHub's OAuth quirks automatically:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github import GitHubAuth
# The GitHubProvider handles GitHub's token format and validation
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="/auth/callback" # Default value, customize if needed
# The GitHubAuth plugin handles GitHub's token format and validation
auth_plugin = GitHubAuth(
GitHubAuth.Config(
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="/auth/callback" # Default value, customize if needed
)
)
mcp = FastMCP(name="GitHub Secured App", auth=auth_provider)
mcp = FastMCP(name="GitHub Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@ -83,7 +85,7 @@ async def get_user_info() -> dict:
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
# The GitHubProvider stores user data in token claims
# The GitHub auth plugin stores user data in token claims
return {
"github_user": token.claims.get("login"),
"name": token.claims.get("name"),
@ -143,19 +145,19 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github import GitHubAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN",
client_secret="github_pat_...",
base_url="https://your-production-domain.com",
# Production token management
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = GitHubAuth(
GitHubAuth.Config(
client_id="Ov23liAbcDefGhiJkLmN",
client_secret="github_pat_...",
base_url="https://your-production-domain.com",
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -165,7 +167,7 @@ auth_provider = GitHubProvider(
)
)
mcp = FastMCP(name="Production GitHub App", auth=auth_provider)
mcp = FastMCP(name="Production GitHub App", plugins=[auth_plugin])
```
<Note>

View file

@ -45,7 +45,7 @@ Create an OAuth 2.0 Client ID in your Google Cloud Console to get the credential
</Warning>
<Tip>
If you want to use a custom callback path (e.g., `/auth/google/callback`), make sure to set the same path in both your Google OAuth Client settings and the `redirect_path` parameter when configuring the GoogleProvider.
If you want to use a custom callback path (e.g., `/auth/google/callback`), make sure to set the same path in both your Google OAuth Client settings and the `redirect_path` parameter when configuring the Google plugin.
</Tip>
</Step>
@ -65,25 +65,27 @@ Create an OAuth 2.0 Client ID in your Google Cloud Console to get the credential
### Step 2: FastMCP Configuration
Create your FastMCP server using the `GoogleProvider`, which handles Google's OAuth flow automatically:
Create your FastMCP server using the `GoogleAuth` plugin, which handles Google's OAuth flow automatically:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.plugins.auth.google import GoogleAuth
# The GoogleProvider handles Google's token format and validation
auth_provider = GoogleProvider(
client_id="123456789.apps.googleusercontent.com", # Your Google OAuth Client ID
client_secret="GOCSPX-abc123...", # Your Google OAuth Client Secret
base_url="http://localhost:8000", # Must match your OAuth configuration
required_scopes=[ # Request user information
"openid",
"https://www.googleapis.com/auth/userinfo.email",
],
# redirect_path="/auth/callback" # Default value, customize if needed
# The GoogleAuth plugin handles Google's token format and validation
auth_plugin = GoogleAuth(
GoogleAuth.Config(
client_id="123456789.apps.googleusercontent.com", # Your Google OAuth Client ID
client_secret="GOCSPX-abc123...", # Your Google OAuth Client Secret
base_url="http://localhost:8000", # Must match your OAuth configuration
required_scopes=[ # Request user information
"openid",
"https://www.googleapis.com/auth/userinfo.email",
],
# redirect_path="/auth/callback" # Default value, customize if needed
)
)
mcp = FastMCP(name="Google Secured App", auth=auth_provider)
mcp = FastMCP(name="Google Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@ -92,7 +94,7 @@ async def get_user_info() -> dict:
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
# The GoogleProvider stores user data in token claims
# The Google auth plugin stores user data in token claims
return {
"google_id": token.claims.get("sub"),
"email": token.claims.get("email"),
@ -156,20 +158,20 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.plugins.auth.google import GoogleAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = GoogleProvider(
client_id="123456789.apps.googleusercontent.com",
client_secret="GOCSPX-abc123...",
base_url="https://your-production-domain.com",
required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
# Production token management
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = GoogleAuth(
GoogleAuth.Config(
client_id="123456789.apps.googleusercontent.com",
client_secret="GOCSPX-abc123...",
base_url="https://your-production-domain.com",
required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -179,11 +181,11 @@ auth_provider = GoogleProvider(
)
)
mcp = FastMCP(name="Production Google App", auth=auth_provider)
mcp = FastMCP(name="Production Google App", plugins=[auth_plugin])
```
<Note>
Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
</Note>
</Note>

View file

@ -27,22 +27,24 @@ Before you begin, you will need:
### FastMCP Configuration
Create your FastMCP server and use `KeycloakAuthProvider` to handle OAuth:
Create your FastMCP server and use the `KeycloakAuth` plugin to handle OAuth:
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
from fastmcp.server.dependencies import get_access_token
auth = KeycloakAuthProvider(
realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/myrealm",
base_url="http://localhost:8000",
# audience="http://localhost:8000", # Recommended for production
auth_plugin = KeycloakAuth(
KeycloakAuth.Config(
realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/myrealm",
base_url="http://localhost:8000",
# audience="http://localhost:8000", # Recommended for production
)
)
mcp = FastMCP("Keycloak Example Server", auth=auth)
mcp = FastMCP("Keycloak Example Server", plugins=[auth_plugin])
@mcp.tool
@ -124,7 +126,7 @@ async def admin_only_tool() -> str:
```python
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
custom_verifier = JWTVerifier(
jwks_uri="http://localhost:8080/realms/myrealm/protocol/openid-connect/certs",
@ -133,9 +135,11 @@ custom_verifier = JWTVerifier(
required_scopes=["api:read", "api:write"],
)
auth = KeycloakAuthProvider(
realm_url="http://localhost:8080/realms/myrealm",
base_url="http://localhost:8000",
auth_plugin = KeycloakAuth(
KeycloakAuth.Config(
realm_url="http://localhost:8080/realms/myrealm",
base_url="http://localhost:8000",
),
token_verifier=custom_verifier,
)
```

View file

@ -87,7 +87,7 @@ Follow the Steps as mentioned below to create an OAuth client.
Click on "Submit" button to update OAuth configuration for the client application.
**Note: You don't need to do any special configuration to support PKCE for the OAuth client.**
Make sure to Activate the client application.
Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in your code.
Note down client ID and client secret for the application. You'll use these values when configuring the OCI plugin in your code.
</Step>
</Steps>
@ -209,7 +209,7 @@ For production deployments with persistent token management across server restar
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.oci import OCIProvider
from fastmcp.server.plugins.auth.oci import OCIAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
@ -217,14 +217,14 @@ from cryptography.fernet import Fernet
# Load configuration from environment
# Production setup with encrypted persistent token storage
auth_provider = OCIProvider(
config_url=os.environ.get("OCI_CONFIG_URL"),
client_id=os.environ.get("OCI_CLIENT_ID"),
client_secret=os.environ.get("OCI_CLIENT_SECRET"),
base_url=os.environ.get("BASE_URL", "https://your-production-domain.com"),
# Production token management
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = OCIAuth(
OCIAuth.Config(
config_url=os.environ.get("OCI_CONFIG_URL"),
client_id=os.environ.get("OCI_CLIENT_ID"),
client_secret=os.environ.get("OCI_CLIENT_SECRET"),
base_url=os.environ.get("BASE_URL", "https://your-production-domain.com"),
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -234,7 +234,7 @@ auth_provider = OCIProvider(
)
)
mcp = FastMCP(name="Production OCI App", auth=auth_provider)
mcp = FastMCP(name="Production OCI App", plugins=[auth_plugin])
```
<Note>
@ -245,4 +245,4 @@ For complete details on these parameters, see the [OAuth Proxy documentation](/s
<Info>
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.
</Info>
</Info>

View file

@ -67,22 +67,24 @@ SERVER_URL=http://localhost:8000 # Your server's base U
### Step 3: FastMCP Configuration
Create your FastMCP server file and use the PropelAuthProvider to handle all the OAuth integration automatically:
Create your FastMCP server file and use the `PropelAuth` plugin to handle the OAuth integration automatically:
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
from fastmcp.server.plugins.auth.propelauth import PropelAuth
auth_provider = PropelAuthProvider(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
base_url=os.environ["SERVER_URL"],
required_scopes=["read:user_data"], # Optional scope enforcement
auth_plugin = PropelAuth(
PropelAuth.Config(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
base_url=os.environ["SERVER_URL"],
required_scopes=["read:user_data"], # Optional scope enforcement
)
)
mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth_provider)
mcp = FastMCP(name="My PropelAuth Protected Server", plugins=[auth_plugin])
```
## Testing
@ -114,18 +116,20 @@ You can use `get_access_token()` inside your tools to identify the authenticated
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
from fastmcp.server.plugins.auth.propelauth import PropelAuth
from fastmcp.server.dependencies import get_access_token
auth = PropelAuthProvider(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
base_url=os.environ["SERVER_URL"],
required_scopes=["read:user_data"],
auth_plugin = PropelAuth(
PropelAuth.Config(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
base_url=os.environ["SERVER_URL"],
required_scopes=["read:user_data"],
)
)
mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth)
mcp = FastMCP(name="My PropelAuth Protected Server", plugins=[auth_plugin])
@mcp.tool
def whoami() -> dict:
@ -139,26 +143,26 @@ def whoami() -> dict:
## Advanced Configuration
The `PropelAuthProvider` supports optional overrides for token introspection behavior, including caching and request timeouts:
The `PropelAuth` plugin supports optional overrides for token introspection behavior, including caching and request timeouts:
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
from fastmcp.server.plugins.auth.propelauth import PropelAuth
auth = PropelAuthProvider(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
required_scopes=["read:user_data"],
resource="https://your-server.com/mcp", # Restrict to tokens intended for this server (RFC 8707)
token_introspection_overrides={
"cache_ttl_seconds": 300, # Cache introspection results for 5 minutes
"max_cache_size": 1000, # Maximum cached tokens
"timeout_seconds": 15, # HTTP request timeout
},
auth_plugin = PropelAuth(
PropelAuth.Config(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
required_scopes=["read:user_data"],
resource="https://your-server.com/mcp", # Restrict to tokens intended for this server (RFC 8707)
introspection_cache_ttl_seconds=300, # Cache introspection results for 5 minutes
introspection_max_cache_size=1000, # Maximum cached tokens
introspection_timeout_seconds=15, # HTTP request timeout
)
)
mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth)
mcp = FastMCP(name="My PropelAuth Protected Server", plugins=[auth_plugin])
```

View file

@ -43,24 +43,26 @@ BASE_URL=http://localhost:8000/
### Step 2: Add auth to FastMCP server
Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:
Create your FastMCP server file and use the `ScalekitAuth` plugin to handle the OAuth integration automatically:
> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
# Discovers Scalekit endpoints and set up JWT token validation
auth_provider = ScalekitProvider(
environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
base_url=SERVER_URL, # Public MCP endpoint
required_scopes=["read"], # Optional scope enforcement
auth_plugin = ScalekitAuth(
ScalekitAuth.Config(
environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
base_url=SERVER_URL, # Public MCP endpoint
required_scopes=["read"], # Optional scope enforcement
)
)
# Create FastMCP server with auth
mcp = FastMCP(name="My Scalekit Protected Server", auth=auth_provider)
# Create FastMCP server with auth plugin
mcp = FastMCP(name="My Scalekit Protected Server", plugins=[auth_plugin])
@mcp.tool
def auth_status() -> dict:
@ -95,16 +97,18 @@ For production deployments, load configuration from environment variables:
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
# Load configuration from environment variables
auth = ScalekitProvider(
environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
resource_id=os.environ.get("SCALEKIT_RESOURCE_ID"),
base_url=os.environ.get("BASE_URL", "https://your-server.com")
auth_plugin = ScalekitAuth(
ScalekitAuth.Config(
environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
resource_id=os.environ.get("SCALEKIT_RESOURCE_ID"),
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
)
)
mcp = FastMCP(name="My Scalekit Protected Server", auth=auth)
mcp = FastMCP(name="My Scalekit Protected Server", plugins=[auth_plugin])
@mcp.tool
def protected_action() -> str:

View file

@ -19,7 +19,7 @@ Supabase Auth does not currently support [RFC 8707](https://www.rfc-editor.org/r
Supabase's OAuth Server delegates the user consent screen to your application. When an MCP client initiates authorization, Supabase authenticates the user and then redirects to your application at a configured callback URL (e.g., `https://your-app.com/oauth/callback?authorization_id=...`). Your application must host a page that calls Supabase's `approveAuthorization()` or `denyAuthorization()` APIs to complete the flow.
`SupabaseProvider` handles the resource server side (token verification and metadata), but you are responsible for building and hosting the consent UI separately. See [Supabase's OAuth Server documentation](https://supabase.com/docs/guides/auth/oauth-server/getting-started) for details on implementing the authorization page.
The Supabase auth plugin handles the resource server side (token verification and metadata), but you are responsible for building and hosting the consent UI separately. See [Supabase's OAuth Server documentation](https://supabase.com/docs/guides/auth/oauth-server/getting-started) for details on implementing the authorization page.
## Configuration
@ -49,18 +49,20 @@ In your Supabase Dashboard:
### Step 3: FastMCP Configuration
Create your FastMCP server using the `SupabaseProvider`:
Create your FastMCP server using the `SupabaseAuth` plugin:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.supabase import SupabaseProvider
from fastmcp.server.plugins.auth.supabase import SupabaseAuth
auth = SupabaseProvider(
project_url="https://abc123.supabase.co",
base_url="http://localhost:8000",
auth_plugin = SupabaseAuth(
SupabaseAuth.Config(
project_url="https://abc123.supabase.co",
base_url="http://localhost:8000",
)
)
mcp = FastMCP("Supabase Protected Server", auth=auth)
mcp = FastMCP("Supabase Protected Server", plugins=[auth_plugin])
@mcp.tool
def protected_tool(message: str) -> str:
@ -112,12 +114,14 @@ For production deployments, load configuration from environment variables:
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.supabase import SupabaseProvider
from fastmcp.server.plugins.auth.supabase import SupabaseAuth
auth = SupabaseProvider(
project_url=os.environ["SUPABASE_PROJECT_URL"],
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
auth_plugin = SupabaseAuth(
SupabaseAuth.Config(
project_url=os.environ["SUPABASE_PROJECT_URL"],
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
)
)
mcp = FastMCP(name="Supabase Secured App", auth=auth)
mcp = FastMCP(name="Supabase Secured App", plugins=[auth_plugin])
```

View file

@ -56,22 +56,24 @@ The callback URL must match exactly. The default path is `/auth/callback`, but y
### Step 2: FastMCP Configuration
Create your FastMCP server using the `WorkOSProvider`:
Create your FastMCP server using the `WorkOSAuth` plugin:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import WorkOSProvider
from fastmcp.server.plugins.auth.workos import WorkOSAuth
# 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"]
auth_plugin = WorkOSAuth(
WorkOSAuth.Config(
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 = FastMCP("WorkOS Protected Server", plugins=[auth_plugin])
@mcp.tool
def protected_tool(message: str) -> str:
@ -134,21 +136,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import WorkOSProvider
from fastmcp.server.plugins.auth.workos import WorkOSAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth = WorkOSProvider(
client_id="client_YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
authkit_domain="https://your-app.authkit.app",
base_url="https://your-production-domain.com",
required_scopes=["openid", "profile", "email"],
# Production token management
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
auth_plugin = WorkOSAuth(
WorkOSAuth.Config(
client_id="client_YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
authkit_domain="https://your-app.authkit.app",
base_url="https://your-production-domain.com",
required_scopes=["openid", "profile", "email"],
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@ -158,7 +160,7 @@ auth = WorkOSProvider(
)
)
mcp = FastMCP(name="Production WorkOS App", auth=auth)
mcp = FastMCP(name="Production WorkOS App", plugins=[auth_plugin])
```
<Note>
@ -197,4 +199,4 @@ OAuth callback path
<ParamField path="timeout_seconds" default="10">
API request timeout
</ParamField>
</Card>
</Card>

View file

@ -106,7 +106,7 @@ For example, the built-in `AuthKitProvider` uses WorkOS AuthKit, which fully sup
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",
@ -136,7 +136,7 @@ For example, the built-in `GitHubProvider` extends `OAuthProxy` to work with Git
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="Ov23li...", # Your GitHub OAuth App ID
@ -221,7 +221,7 @@ For production deployments, load sensitive values like client secrets from envir
```python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
# Load secrets from environment variables
auth = GitHubProvider(

View file

@ -355,7 +355,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
FastMCP includes pre-configured providers for common services:
```python
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-github-app-id",
@ -690,7 +690,7 @@ For production deployments, load sensitive credentials from environment variable
```python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
# Load secrets from environment variables
auth = GitHubProvider(

View file

@ -218,7 +218,7 @@ auth = OIDCProxy(
FastMCP includes pre-configured OIDC providers for common services:
```python
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
auth = Auth0Provider(
config_url="https://.../.well-known/openid-configuration",
@ -262,7 +262,7 @@ For production deployments, load sensitive credentials from environment variable
```python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
# Load secrets from environment variables
auth = Auth0Provider(

View file

@ -10,68 +10,53 @@ Use an auth plugin when you want authentication to be configured alongside other
```python server.py
from fastmcp import FastMCP
from fastmcp.server.plugins.auth import GitHubAuth
from fastmcp.server.plugins.auth.github import GitHubAuth
mcp = FastMCP(
"GitHub Protected Server",
plugins=[
GitHubAuth(
{
"client_id": "your-github-client-id",
"client_secret": "your-github-client-secret",
"base_url": "https://your-server.com",
}
GitHubAuth.Config(
client_id="your-github-client-id",
client_secret="your-github-client-secret",
base_url="https://your-server.com",
)
)
],
)
```
The provider APIs remain available and are still the most direct option in Python code:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
auth = GitHubProvider(
client_id="your-github-client-id",
client_secret="your-github-client-secret",
base_url="https://your-server.com",
)
mcp = FastMCP("GitHub Protected Server", auth=auth)
```
Provider APIs remain available in each plugin's explicit `.provider` module for advanced direct auth wiring, but integrations should prefer the plugin form.
## Included Plugins
Import first-party auth plugins from `fastmcp.server.plugins.auth`:
Each first-party auth plugin lives in its own module under `fastmcp.server.plugins.auth`, mirroring the provider package:
```python
from fastmcp.server.plugins.auth import (
Auth0Auth,
AuthKitAuth,
AWSCognitoAuth,
AzureAuth,
ClerkAuth,
DescopeAuth,
DiscordAuth,
GitHubAuth,
GoogleAuth,
KeycloakAuth,
OCIAuth,
PropelAuth,
ScalekitAuth,
SupabaseAuth,
WorkOSAuth,
)
from fastmcp.server.plugins.auth.auth0 import Auth0Auth
from fastmcp.server.plugins.auth.authkit import AuthKitAuth
from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
from fastmcp.server.plugins.auth.azure import AzureAuth
from fastmcp.server.plugins.auth.clerk import ClerkAuth
from fastmcp.server.plugins.auth.descope import DescopeAuth
from fastmcp.server.plugins.auth.discord import DiscordAuth
from fastmcp.server.plugins.auth.github import GitHubAuth
from fastmcp.server.plugins.auth.google import GoogleAuth
from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
from fastmcp.server.plugins.auth.oci import OCIAuth
from fastmcp.server.plugins.auth.propelauth import PropelAuth
from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
from fastmcp.server.plugins.auth.supabase import SupabaseAuth
from fastmcp.server.plugins.auth.workos import WorkOSAuth
```
Each plugin accepts a matching `*AuthConfig` model or a plain dictionary. Config fields mirror the wrapped provider's constructor wherever the value can be represented as JSON. Python-only objects such as custom token verifiers, HTTP clients, and client storage are passed as constructor keyword arguments:
Each plugin exposes its serializable configuration model as `Plugin.Config`. Config fields mirror the wrapped provider's constructor wherever the value can be represented as JSON. Python-only objects such as custom token verifiers, HTTP clients, and client storage are passed as constructor keyword arguments:
```python
from fastmcp.server.plugins.auth import SupabaseAuth, SupabaseAuthConfig
from fastmcp.server.plugins.auth.supabase import SupabaseAuth
auth_plugin = SupabaseAuth(
SupabaseAuthConfig(
SupabaseAuth.Config(
project_url="https://abc123.supabase.co",
base_url="https://your-server.com",
required_scopes=["read"],

View file

@ -197,7 +197,7 @@ WorkOS AuthKit provides an excellent example of remote OAuth integration. The `A
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",

View file

@ -111,7 +111,7 @@ For OAuth token storage:
```python
import os
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
auth = GitHubProvider(
@ -163,7 +163,7 @@ By default, FastMCP automatically manages keys and storage based on your platfor
No configuration needed:
```python
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-id",
@ -178,7 +178,7 @@ For production deployments, configure explicit keys and persistent network-acces
```python
import os
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet

View file

@ -468,7 +468,7 @@ When mounting an OAuth-protected server under a path prefix, declare your URLs u
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
@ -530,7 +530,7 @@ Here's a complete working example showing all the pieces together:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
import uvicorn

View file

@ -81,7 +81,7 @@ Create your FastMCP server using the `Auth0Provider`.
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
# The Auth0Provider utilizes Auth0 OIDC configuration
auth_provider = Auth0Provider(
@ -158,7 +158,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@ -205,7 +205,7 @@ Setting this environment variable allows the Auth0 provider to be used automatic
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.auth0.Auth0Provider` to use Auth0 authentication.
Set to `fastmcp.server.plugins.auth.auth0.provider.Auth0Provider` to use Auth0 authentication.
</ParamField>
</Card>
@ -250,7 +250,7 @@ Comma-, space-, or JSON-separated list of required AUth0 scopes (e.g., `openid e
Example `.env` file:
```bash
# Use the Auth0 provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.auth0.provider.Auth0Provider
# Auth0 configuration and credentials
FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration

View file

@ -43,7 +43,7 @@ Create your FastMCP server file and use the `AuthKitProvider` to handle all the
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
# The AuthKitProvider automatically discovers WorkOS endpoints
# and configures JWT token validation
@ -90,7 +90,7 @@ Setting this environment variable allows the AuthKit provider to be used automat
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.workos.AuthKitProvider` to use AuthKit authentication.
Set to `fastmcp.server.plugins.auth.authkit.provider.AuthKitProvider` to use AuthKit authentication.
</ParamField>
</Card>
@ -115,7 +115,7 @@ Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid p
Example `.env` file:
```bash
# Use the AuthKit provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.AuthKitProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.authkit.provider.AuthKitProvider
# AuthKit configuration
FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN=https://your-project-12345.authkit.app

View file

@ -121,7 +121,7 @@ Create your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cog
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.aws import AWSCognitoProvider
from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
from fastmcp.server.dependencies import get_access_token
# The AWSCognitoProvider handles JWT validation and user claims
@ -205,7 +205,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.aws import AWSCognitoProvider
from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@ -248,7 +248,7 @@ Setting this environment variable allows the AWS Cognito provider to be used aut
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.aws.AWSCognitoProvider` to use AWS Cognito authentication.
Set to `fastmcp.server.plugins.auth.aws.provider.AWSCognitoProvider` to use AWS Cognito authentication.
</ParamField>
</Card>
@ -293,7 +293,7 @@ Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid e
Example `.env` file:
```bash
# Use the AWS Cognito provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.aws.AWSCognitoProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.aws.provider.AWSCognitoProvider
# AWS Cognito credentials
FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID=eu-central-1_XXXXXXXXX

View file

@ -114,7 +114,7 @@ Create your FastMCP server using the `AzureProvider`, which handles Azure's OAut
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.plugins.auth.azure.provider import AzureProvider
# The AzureProvider handles Azure's token format and validation
auth_provider = AzureProvider(
@ -247,7 +247,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.plugins.auth.azure.provider import AzureProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@ -292,7 +292,7 @@ Setting this environment variable allows the Azure provider to be used automatic
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.azure.AzureProvider` to use Azure authentication.
Set to `fastmcp.server.plugins.auth.azure.provider.AzureProvider` to use Azure authentication.
</ParamField>
</Card>
@ -360,7 +360,7 @@ This setting affects all Azure OAuth endpoints (authorization, token, issuer, JW
Example `.env` file:
```bash
# Use the Azure provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.azure.AzureProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.azure.provider.AzureProvider
# Azure OAuth credentials
FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID=835f09b6-0f0f-40cc-85cb-f32c5829a149

View file

@ -59,7 +59,7 @@ Create your FastMCP server file and use the DescopeProvider to handle all the OA
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.descope import DescopeProvider
from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
# The DescopeProvider automatically discovers Descope endpoints
# and configures JWT token validation
@ -105,7 +105,7 @@ Setting this environment variable allows the Descope provider to be used automat
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.descope.DescopeProvider` to use
Set to `fastmcp.server.plugins.auth.descope.provider.DescopeProvider` to use
Descope authentication.
</ParamField>
</Card>
@ -129,7 +129,7 @@ Example `.env` file:
```bash
# Use the Descope provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.descope.DescopeProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.descope.provider.DescopeProvider
# Descope configuration
FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration

View file

@ -61,7 +61,7 @@ Create your FastMCP server using the `DiscordProvider`, which handles Discord's
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.discord import DiscordProvider
from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
auth_provider = DiscordProvider(
client_id="12345", # Your Discord Application Client ID
@ -154,7 +154,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.discord import DiscordProvider
from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@ -193,7 +193,7 @@ Setting this environment variable allows the Discord provider to be used automat
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.discord.DiscordProvider` to use Discord authentication.
Set to `fastmcp.server.plugins.auth.discord.provider.DiscordProvider` to use Discord authentication.
</ParamField>
</Card>
@ -233,7 +233,7 @@ HTTP request timeout for Discord API calls
Example `.env` file:
```bash
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.discord.DiscordProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.discord.provider.DiscordProvider
FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID=12345
FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET=your-client-secret

View file

@ -65,7 +65,7 @@ Create your FastMCP server using the `GitHubProvider`, which handles GitHub's OA
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
# The GitHubProvider handles GitHub's token format and validation
auth_provider = GitHubProvider(
@ -144,7 +144,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@ -187,7 +187,7 @@ Setting this environment variable allows the GitHub provider to be used automati
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.github.GitHubProvider` to use GitHub authentication.
Set to `fastmcp.server.plugins.auth.github.provider.GitHubProvider` to use GitHub authentication.
</ParamField>
</Card>
@ -228,7 +228,7 @@ HTTP request timeout for GitHub API calls
Example `.env` file:
```bash
# Use the GitHub provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.github.provider.GitHubProvider
# GitHub OAuth credentials
FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=Ov23liAbcDefGhiJkLmN

View file

@ -70,7 +70,7 @@ Create your FastMCP server using the `GoogleProvider`, which handles Google's OA
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.plugins.auth.google.provider import GoogleProvider
# The GoogleProvider handles Google's token format and validation
auth_provider = GoogleProvider(
@ -157,7 +157,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.plugins.auth.google.provider import GoogleProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@ -201,7 +201,7 @@ Setting this environment variable allows the Google provider to be used automati
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.google.GoogleProvider` to use Google authentication.
Set to `fastmcp.server.plugins.auth.google.provider.GoogleProvider` to use Google authentication.
</ParamField>
</Card>
@ -242,7 +242,7 @@ HTTP request timeout for Google API calls
Example `.env` file:
```bash
# Use the Google provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.google.GoogleProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.google.provider.GoogleProvider
# Google OAuth credentials
FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com

View file

@ -213,7 +213,7 @@ For production deployments with persistent token management across server restar
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.oci import OCIProvider
from fastmcp.server.plugins.auth.oci.provider import OCIProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
@ -265,7 +265,7 @@ Setting this environment variable allows the OCI provider to be used automatical
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.oci.OCIProvider` to use OCI IAM authentication.
Set to `fastmcp.server.plugins.auth.oci.provider.OCIProvider` to use OCI IAM authentication.
</ParamField>
</Card>
@ -303,7 +303,7 @@ Redirect path configured in your OCI IAM Integrated Application
Example `.env` file:
```bash
# Use the OCI IAM provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.oci.OCIProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.oci.provider.OCIProvider
# OCI IAM configuration and credentials
FASTMCP_SERVER_AUTH_OCI_IAM_GUID=idcs-asaacasd1111.....

View file

@ -50,7 +50,7 @@ Create your FastMCP server file and use the ScalekitProvider to handle all the O
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
# Discovers Scalekit endpoints and set up JWT token validation
auth_provider = ScalekitProvider(
@ -95,7 +95,7 @@ Setting this environment variable allows the Scalekit provider to be used automa
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.scalekit.ScalekitProvider` to use Scalekit authentication.
Set to `fastmcp.server.plugins.auth.scalekit.provider.ScalekitProvider` to use Scalekit authentication.
</ParamField>
</Card>
@ -127,7 +127,7 @@ Example `.env`:
```bash
# Use the Scalekit provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.scalekit.ScalekitProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.scalekit.provider.ScalekitProvider
# Scalekit configuration
FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL=https://your-env.scalekit.com

View file

@ -32,7 +32,7 @@ Create your FastMCP server using the `SupabaseProvider`:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.supabase import SupabaseProvider
from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
# Configure Supabase Auth
auth = SupabaseProvider(
@ -101,7 +101,7 @@ Setting this environment variable allows the Supabase provider to be used automa
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.supabase.SupabaseProvider` to use Supabase authentication.
Set to `fastmcp.server.plugins.auth.supabase.provider.SupabaseProvider` to use Supabase authentication.
</ParamField>
</Card>
@ -130,7 +130,7 @@ Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid e
Example `.env` file:
```bash
# Use the Supabase provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.supabase.SupabaseProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.supabase.provider.SupabaseProvider
# Supabase configuration
FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL=https://abc123.supabase.co

View file

@ -61,7 +61,7 @@ Create your FastMCP server using the `WorkOSProvider`:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import WorkOSProvider
from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
# Configure WorkOS OAuth
auth = WorkOSProvider(
@ -135,7 +135,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import WorkOSProvider
from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@ -180,7 +180,7 @@ Setting this environment variable allows the WorkOS provider to be used automati
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.workos.WorkOSProvider` to use WorkOS authentication.
Set to `fastmcp.server.plugins.auth.workos.provider.WorkOSProvider` to use WorkOS authentication.
</ParamField>
</Card>
@ -232,7 +232,7 @@ 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=fastmcp.server.auth.providers.workos.WorkOSProvider
FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.workos.provider.WorkOSProvider
```
With environment variables set, you can either:
@ -240,14 +240,14 @@ 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
from fastmcp.server.plugins.auth.workos.provider 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=fastmcp.server.auth.providers.workos.WorkOSProvider)**
**Option 2: Automatic provisioning (requires FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.workos.provider.WorkOSProvider)**
```python server.py
from fastmcp import FastMCP

View file

@ -106,7 +106,7 @@ For example, the built-in `AuthKitProvider` uses WorkOS AuthKit, which fully sup
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",
@ -136,7 +136,7 @@ For example, the built-in `GitHubProvider` extends `OAuthProxy` to work with Git
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="Ov23li...", # Your GitHub OAuth App ID
@ -202,11 +202,11 @@ Authentication providers are configured by specifying the full module path to th
<ParamField path="FASTMCP_SERVER_AUTH" type="string">
The full module path to the authentication provider class. Examples:
- `fastmcp.server.auth.providers.github.GitHubProvider` - GitHub OAuth
- `fastmcp.server.auth.providers.google.GoogleProvider` - Google OAuth
- `fastmcp.server.plugins.auth.github.provider.GitHubProvider` - GitHub OAuth
- `fastmcp.server.plugins.auth.google.provider.GoogleProvider` - Google OAuth
- `fastmcp.server.auth.providers.jwt.JWTVerifier` - JWT token verification
- `fastmcp.server.auth.providers.workos.WorkOSProvider` - WorkOS OAuth
- `fastmcp.server.auth.providers.workos.AuthKitProvider` - WorkOS AuthKit
- `fastmcp.server.plugins.auth.workos.provider.WorkOSProvider` - WorkOS OAuth
- `fastmcp.server.plugins.auth.authkit.provider.AuthKitProvider` - WorkOS AuthKit
- `mycompany.auth.CustomProvider` - Your custom provider class
</ParamField>
@ -214,12 +214,12 @@ When using providers like GitHub or Google, you'll need to set provider-specific
```bash
# GitHub OAuth
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.github.provider.GitHubProvider
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID="Ov23li..."
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET="github_pat_..."
# Google OAuth
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.google.GoogleProvider
export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.google.provider.GoogleProvider
export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID="123456.apps.googleusercontent.com"
export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET="GOCSPX-..."
```

View file

@ -345,7 +345,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
FastMCP includes pre-configured providers for common services:
```python
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-github-app-id",
@ -590,7 +590,7 @@ For production deployments, configure the OAuth proxy through environment variab
```bash
# Specify the provider implementation
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.github.provider.GitHubProvider
# Provider-specific credentials
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID="Ov23li..."

View file

@ -218,7 +218,7 @@ auth = OIDCProxy(
FastMCP includes pre-configured OIDC providers for common services:
```python
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
auth = Auth0Provider(
config_url="https://.../.well-known/openid-configuration",
@ -247,7 +247,7 @@ For production deployments, configure the OIDC proxy through environment variabl
```bash
# Specify the provider implementation
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider
export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.auth0.provider.Auth0Provider
# Provider-specific credentials
export FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration

View file

@ -183,7 +183,7 @@ WorkOS AuthKit provides an excellent example of remote OAuth integration. The `A
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",

View file

@ -57,7 +57,7 @@ middleware = ResponseCachingMiddleware(
Or with OAuth token storage:
```python
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.disk import DiskStore
auth = GitHubProvider(
@ -110,7 +110,7 @@ For OAuth token storage:
```python
import os
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
auth = GitHubProvider(
@ -162,7 +162,7 @@ By default, FastMCP automatically manages keys and storage based on your platfor
No configuration needed:
```python
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-id",
@ -177,7 +177,7 @@ For production deployments, configure explicit keys and persistent network-acces
```python
import os
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet

View file

@ -1,135 +1,20 @@
"""Auth0 OAuth provider for FastMCP.
"""Backward compatibility shim for Auth0 auth provider."""
This module provides a complete Auth0 integration that's ready to use with
just the configuration URL, client ID, client secret, audience, and base URL.
from __future__ import annotations
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
import warnings
# Simple Auth0 OAuth protection
auth = Auth0Provider(
config_url="https://auth0.config.url",
client_id="your-auth0-client-id",
client_secret="your-auth0-client-secret",
audience="your-auth0-api-audience",
base_url="http://localhost:8000",
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.auth0 is deprecated. "
"Import from fastmcp.server.plugins.auth.auth0.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
from typing import Literal
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class Auth0Provider(OIDCProxy):
"""An Auth0 provider implementation for FastMCP.
This provider is a complete Auth0 integration that's ready to use with
just the configuration URL, client ID, client secret, audience, and base URL.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
# Simple Auth0 OAuth protection
auth = Auth0Provider(
config_url="https://auth0.config.url",
client_id="your-auth0-client-id",
client_secret="your-auth0-client-secret",
audience="your-auth0-api-audience",
base_url="http://localhost:8000",
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
def __init__(
self,
*,
config_url: AnyHttpUrl | str,
client_id: str,
client_secret: str,
audience: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
redirect_path: str | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
) -> None:
"""Initialize Auth0 OAuth provider.
Args:
config_url: Auth0 config URL
client_id: Auth0 application client id
client_secret: Auth0 application client secret
audience: Auth0 API audience
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
required_scopes: Required Auth0 scopes (defaults to ["openid"])
redirect_path: Redirect path configured in Auth0 application
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Auth0.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
"""
# Parse scopes if provided as string
auth0_required_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
super().__init__(
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
audience=audience,
base_url=base_url,
resource_base_url=resource_base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
required_scopes=auth0_required_scopes,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
)
logger.debug(
"Initialized Auth0 OAuth provider for client %s with scopes: %s",
client_id,
auth0_required_scopes,
)
__all__ = ["Auth0Provider"]

View file

@ -1,229 +1,23 @@
"""AWS Cognito OAuth provider for FastMCP.
This module provides a complete AWS Cognito OAuth integration that's ready to use
with a user pool ID, domain prefix, client ID and client secret. It handles all
the complexity of AWS Cognito's OAuth flow, token validation, and user management.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
# Simple AWS Cognito OAuth protection
auth = AWSCognitoProvider(
user_pool_id="your-user-pool-id",
aws_region="eu-central-1",
client_id="your-cognito-client-id",
client_secret="your-cognito-client-secret"
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
"""Backward compatibility shim for AWS Cognito auth provider."""
from __future__ import annotations
from typing import Literal
import warnings
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.aws is deprecated. "
"Import from fastmcp.server.plugins.auth.aws.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.aws.provider import (
AWSCognitoProvider,
AWSCognitoTokenVerifier,
)
class AWSCognitoTokenVerifier(JWTVerifier):
"""Token verifier for Cognito access tokens.
Cognito access tokens use a ``client_id`` claim instead of the
standard ``aud`` claim. This subclass passes ``audience=None``
to the parent (skipping the ``aud`` check) and validates the
``client_id`` claim directly.
"""
def __init__(self, *, audience: str | list[str] | None = None, **kwargs):
self._expected_client_id = audience
super().__init__(audience=None, **kwargs)
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify token and filter claims to Cognito-specific subset."""
access_token = await super().verify_token(token)
if not access_token:
return None
# Validate client_id claim (Cognito's equivalent of aud)
if self._expected_client_id:
token_client_id = access_token.claims.get("client_id")
if isinstance(self._expected_client_id, list):
valid = token_client_id in self._expected_client_id
else:
valid = token_client_id == self._expected_client_id
if not valid:
self.logger.debug(
"Token validation failed: client_id mismatch (expected %s, got %s)",
self._expected_client_id,
token_client_id,
)
return None
# Filter claims to Cognito-specific subset
cognito_claims = {
"sub": access_token.claims.get("sub"),
"username": access_token.claims.get("username"),
"cognito:groups": access_token.claims.get("cognito:groups", []),
}
return AccessToken(
token=access_token.token,
client_id=access_token.client_id,
scopes=access_token.scopes,
expires_at=access_token.expires_at,
claims=cognito_claims,
)
class AWSCognitoProvider(OIDCProxy):
"""Complete AWS Cognito OAuth provider for FastMCP.
This provider makes it trivial to add AWS Cognito OAuth protection to any
FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,
client credentials, and a base URL, and you're ready to go.
Features:
- Automatic OIDC Discovery from AWS Cognito User Pool
- Automatic JWT token validation via Cognito's public keys
- Cognito-specific claim filtering (sub, username, cognito:groups)
- Support for Cognito User Pools
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
auth = AWSCognitoProvider(
user_pool_id="eu-central-1_XXXXXXXXX",
aws_region="eu-central-1",
client_id="your-cognito-client-id",
client_secret="your-cognito-client-secret",
base_url="https://my-server.com",
redirect_path="/custom/callback",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
user_pool_id: str,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
aws_region: str = "eu-central-1",
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str = "/auth/callback",
required_scopes: list[str] | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
):
"""Initialize AWS Cognito OAuth provider.
Args:
user_pool_id: Your Cognito User Pool ID (e.g., "eu-central-1_XXXXXXXXX")
client_id: Cognito app client ID
client_secret: Cognito app client secret
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
required_scopes: Required Cognito scopes (defaults to ["openid"])
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to AWS Cognito.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
"""
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
# Construct OIDC discovery URL
config_url = f"https://cognito-idp.{aws_region}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration"
# Store Cognito-specific info for claim filtering
self.user_pool_id = user_pool_id
self.aws_region = aws_region
self.client_id = client_id
# Initialize OIDC proxy with Cognito discovery
super().__init__(
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
algorithm="RS256",
required_scopes=required_scopes_final,
base_url=base_url,
resource_base_url=resource_base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
)
logger.debug(
"Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)
def get_token_verifier(
self,
*,
algorithm: str | None = None,
audience: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int | None = None,
) -> AWSCognitoTokenVerifier:
"""Creates a Cognito-specific token verifier with claim filtering.
Args:
algorithm: Optional token verifier algorithm
audience: Optional token verifier audience
required_scopes: Optional token verifier required_scopes
timeout_seconds: HTTP request timeout in seconds
"""
return AWSCognitoTokenVerifier(
issuer=str(self.oidc_config.issuer),
audience=audience or self.client_id,
algorithm=algorithm,
jwks_uri=str(self.oidc_config.jwks_uri),
required_scopes=required_scopes,
)
__all__ = ["AWSCognitoProvider", "AWSCognitoTokenVerifier"]

View file

@ -1,768 +1,24 @@
"""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.
"""
"""Backward compatibility shim for Azure auth provider."""
from __future__ import annotations
import hashlib
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Literal, cast
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.dependencies import Dependency
from fastmcp.server.auth.auth import MultiAuth
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from azure.identity.aio import OnBehalfOfCredential
from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyHttpUrl
from fastmcp.server.auth.auth import AuthProvider
logger = get_logger(__name__)
# Standard OIDC scopes that should never be prefixed with identifier_uri.
# Per Microsoft docs: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
# "OIDC scopes are requested as simple string identifiers without resource prefixes"
OIDC_SCOPES = frozenset({"openid", "profile", "email", "offline_access"})
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.
Scope Handling:
- required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
Automatically prefixed with identifier_uri during initialization
Validated on all tokens and advertised to MCP clients
- additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
NOT prefixed, NOT validated, NOT advertised to clients
Used to request Microsoft Graph or other upstream API permissions
Features:
- OAuth proxy to Azure/Microsoft identity platform
- JWT validation using tenant issuer and JWKS
- Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
- Custom API scopes and Microsoft Graph scopes in a single provider
Setup:
1. Create an App registration in Azure Portal
2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
4. Add custom scopes (e.g., "read", "write") under "Expose an API"
5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
6. Create a client secret
7. Get Application (client) ID, Directory (tenant) ID, and client secret
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
# Standard Azure (Public Cloud)
auth = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
required_scopes=["read", "write"], # Unprefixed scope names
additional_authorize_scopes=["User.Read", "Mail.Read"], # Optional Graph scopes
base_url="http://localhost:8000",
# identifier_uri defaults to api://{client_id}
)
# Azure Government
auth_gov = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
required_scopes=["read", "write"],
base_authority="login.microsoftonline.us", # Override for Azure Gov
base_url="http://localhost:8000",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str | None = None,
tenant_id: str,
required_scopes: list[str],
base_url: str,
resource_base_url: AnyHttpUrl | str | None = None,
identifier_uri: str | None = None,
issuer_url: str | None = None,
redirect_path: str | None = None,
additional_authorize_scopes: list[str] | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
base_authority: str = "login.microsoftonline.com",
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
) -> None:
"""Initialize Azure OAuth provider.
Args:
client_id: Azure application (client) ID from your App registration
client_secret: Azure client secret from your App registration. Optional when
using alternative credentials (e.g., managed identity with a custom
_create_upstream_oauth_client override). When omitted, jwt_signing_key
must be provided.
tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers")
identifier_uri: Optional Application ID URI for your custom API (defaults to api://{client_id}).
This URI is automatically prefixed to all required_scopes during initialization.
Example: identifier_uri="api://my-api" + required_scopes=["read"]
tokens validated for "api://my-api/read"
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
For Azure Government, use "login.microsoftonline.us".
required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
- Automatically prefixed with identifier_uri during initialization
- Validated on all tokens
- Advertised in Protected Resource Metadata
- Must match scope names defined in Azure Portal under "Expose an API"
Example: ["read", "write"] validates tokens containing ["api://xxx/read", "api://xxx/write"]
additional_authorize_scopes: Microsoft Graph or other upstream scopes in full format.
- NOT prefixed with identifier_uri
- NOT validated on tokens
- NOT advertised to MCP clients
- Used to request additional permissions from Azure (e.g., Graph API access)
Example: ["User.Read", "Mail.Read"]
These scopes allow your FastMCP server to call Microsoft Graph APIs using the
upstream Azure token, but MCP clients are unaware of them.
Note: "offline_access" is automatically included to obtain refresh tokens.
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Azure.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in JWKS fetches.
When provided, the client is reused for JWT key fetches and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per fetch.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
parsed_required_scopes = parse_scopes(required_scopes)
parsed_additional_scopes: list[str] = (
parse_scopes(additional_authorize_scopes) or []
if additional_authorize_scopes
else []
)
# Always include offline_access to get refresh tokens from Azure
if "offline_access" not in parsed_additional_scopes:
parsed_additional_scopes = [*parsed_additional_scopes, "offline_access"]
# Store Azure-specific config for OBO credential creation
self._tenant_id = tenant_id
self._base_authority = base_authority
# Cache of OBO credentials keyed by hash of user assertion token.
# Reusing credentials allows the Azure SDK's internal token cache
# to avoid redundant OBO exchanges for the same user + scopes.
self._obo_credentials: OrderedDict[str, OnBehalfOfCredential] = OrderedDict()
self._obo_max_credentials: int = 128
# Apply defaults
self.identifier_uri = identifier_uri or f"api://{client_id}"
self.additional_authorize_scopes: list[str] = parsed_additional_scopes
# Always validate tokens against the app's API client ID using JWT
issuer = f"https://{base_authority}/{tenant_id}/v2.0"
jwks_uri = f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys"
# Azure access tokens only include custom API scopes in the `scp` claim,
# NOT standard OIDC scopes (openid, profile, email, offline_access).
# Filter out OIDC scopes from validation - they'll still be sent to Azure
# during authorization (handled by _prefix_scopes_for_azure).
validation_scopes = [
s for s in (parsed_required_scopes or []) if s not in OIDC_SCOPES
]
if not validation_scopes:
raise ValueError(
"AzureProvider requires at least one non-OIDC scope in "
"required_scopes (e.g., 'read', 'write'). OIDC scopes like "
"'openid', 'profile', 'email', and 'offline_access' are not "
"included in Azure access token claims and cannot be used for "
"scope enforcement."
)
token_verifier = JWTVerifier(
jwks_uri=jwks_uri,
issuer=issuer,
audience=[client_id, self.identifier_uri],
algorithm="RS256",
required_scopes=validation_scopes, # Only validate non-OIDC scopes
http_client=http_client,
)
# Build Azure OAuth endpoints with tenant
authorization_endpoint = (
f"https://{base_authority}/{tenant_id}/oauth2/v2.0/authorize"
)
token_endpoint = f"https://{base_authority}/{tenant_id}/oauth2/v2.0/token"
# Initialize OAuth proxy with Azure endpoints
# Remember there's hooks called, such as _prepare_scopes_for_token_exchange
# and _prepare_scopes_for_upstream_refresh
super().__init__(
upstream_authorization_endpoint=authorization_endpoint,
upstream_token_endpoint=token_endpoint,
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
valid_scopes=parsed_required_scopes,
enable_cimd=enable_cimd,
)
authority_info = ""
if base_authority != "login.microsoftonline.com":
authority_info = f" using authority {base_authority}"
logger.info(
"Initialized Azure OAuth provider for client %s with tenant %s%s%s",
client_id,
tenant_id,
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
authority_info,
)
async def authorize(
self,
client: OAuthClientInformationFull,
params: AuthorizationParams,
) -> str:
"""Start OAuth transaction and redirect to Azure AD.
Override parent's authorize method to filter out the 'resource' parameter
which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
scopes to determine the resource/audience instead of a separate parameter.
Args:
client: OAuth client information
params: Authorization parameters from the client
Returns:
Authorization URL to redirect the user to Azure AD
"""
# Clear the resource parameter that Azure AD v2.0 doesn't support
# This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
# but Azure AD v2.0 uses scopes instead to determine the audience
params_to_use = params
if hasattr(params, "resource"):
original_resource = getattr(params, "resource", None)
if original_resource is not None:
params_to_use = params.model_copy(update={"resource": None})
if original_resource:
logger.debug(
"Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
original_resource,
)
# Don't modify the scopes in params - they stay unprefixed for MCP clients
# We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
auth_url = await super().authorize(client, params_to_use)
separator = "&" if "?" in auth_url else "?"
return f"{auth_url}{separator}prompt=select_account"
def _prefix_scopes_for_azure(self, scopes: list[str]) -> list[str]:
"""Prefix unprefixed custom API scopes with identifier_uri for Azure.
This helper centralizes the scope prefixing logic used in both
authorization and token refresh flows.
Scopes that are NOT prefixed:
- Standard OIDC scopes (openid, profile, email, offline_access)
- Fully-qualified URIs (contain "://")
- Scopes with path component (contain "/")
Note: Microsoft Graph scopes (e.g., User.Read) should be passed via
`additional_authorize_scopes` or use fully-qualified format
(e.g., https://graph.microsoft.com/User.Read).
Args:
scopes: List of scopes, may be prefixed or unprefixed
Returns:
List of scopes with identifier_uri prefix applied where needed
"""
prefixed = []
for scope in scopes:
if scope in OIDC_SCOPES:
# Standard OIDC scopes - never prefix
prefixed.append(scope)
elif "://" in scope or "/" in scope:
# Already fully-qualified (e.g., "api://xxx/read" or
# "https://graph.microsoft.com/User.Read")
prefixed.append(scope)
else:
# Unprefixed custom API scope - prefix with identifier_uri
prefixed.append(f"{self.identifier_uri}/{scope}")
return prefixed
def _build_upstream_authorize_url(
self, txn_id: str, transaction: dict[str, Any]
) -> str:
"""Build Azure authorization URL with prefixed scopes.
Overrides parent to prefix scopes with identifier_uri before sending to Azure,
while keeping unprefixed scopes in the transaction for MCP clients.
"""
# Get unprefixed scopes from transaction
unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
# Prefix scopes for Azure authorization request
prefixed_scopes = self._prefix_scopes_for_azure(unprefixed_scopes)
# Add Microsoft Graph scopes (not validated, not prefixed)
if self.additional_authorize_scopes:
prefixed_scopes.extend(self.additional_authorize_scopes)
# Temporarily modify transaction dict for parent's URL building
modified_transaction = transaction.copy()
modified_transaction["scopes"] = prefixed_scopes
# Let parent build the URL with prefixed scopes
return super()._build_upstream_authorize_url(txn_id, modified_transaction)
def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]:
"""Prepare scopes for Azure authorization code exchange.
Azure requires scopes during token exchange (AADSTS28003 error if missing).
Azure only allows ONE resource per token request (AADSTS28000), so we only
include scopes for this API plus OIDC scopes.
Args:
scopes: Scopes from the authorization request (unprefixed)
Returns:
List of scopes for Azure token endpoint
"""
# Prefix scopes for this API
prefixed_scopes = self._prefix_scopes_for_azure(scopes or [])
# Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
if self.additional_authorize_scopes:
prefixed_scopes.extend(
s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
)
deduplicated = list(dict.fromkeys(prefixed_scopes))
logger.debug("Token exchange scopes: %s", deduplicated)
return deduplicated
def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:
"""Prepare scopes for Azure token refresh.
Azure requires fully-qualified scopes and only allows ONE resource per
token request (AADSTS28000). We include scopes for this API plus OIDC scopes.
Args:
scopes: Base scopes from RefreshToken (unprefixed, e.g., ["read"])
Returns:
Deduplicated list of scopes formatted for Azure token endpoint
"""
logger.debug("Base scopes from storage: %s", scopes)
# Filter out any additional_authorize_scopes that may have been stored
additional_scopes_set = set(self.additional_authorize_scopes or [])
base_scopes = [s for s in scopes if s not in additional_scopes_set]
# Prefix base scopes with identifier_uri for Azure
prefixed_scopes = self._prefix_scopes_for_azure(base_scopes)
# Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
if self.additional_authorize_scopes:
prefixed_scopes.extend(
s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
)
deduplicated_scopes = list(dict.fromkeys(prefixed_scopes))
logger.debug("Scopes for Azure token endpoint: %s", deduplicated_scopes)
return deduplicated_scopes
async def _extract_upstream_claims(
self, idp_tokens: dict[str, Any]
) -> dict[str, Any] | None:
"""Extract claims from Azure token response to embed in FastMCP JWT.
Decodes the Azure access token (which is a JWT) to extract user identity
claims. This allows gateways to inspect upstream identity information by
decoding the FastMCP JWT without needing server-side storage lookups.
Azure access tokens contain claims like:
- sub: Subject identifier (unique per user per application)
- oid: Object ID (unique user identifier across Azure AD)
- tid: Tenant ID
- azp: Authorized party (client ID that requested the token)
- name: Display name
- given_name: First name
- family_name: Last name
- preferred_username: User principal name (email format)
- upn: User Principal Name
- email: Email address (if available)
- roles: Application roles assigned to the user
- groups: Group memberships (if configured)
Args:
idp_tokens: Full token response from Azure, containing access_token
and potentially id_token.
Returns:
Dict of extracted claims, or None if extraction fails.
"""
access_token = idp_tokens.get("access_token")
if not access_token:
return None
try:
# Azure access tokens are JWTs - decode without verification
# (already validated by token_verifier during token exchange)
payload = decode_jwt_payload(access_token)
# Extract useful identity claims
claims: dict[str, Any] = {}
claim_keys = [
"sub",
"oid",
"tid",
"azp",
"name",
"given_name",
"family_name",
"preferred_username",
"upn",
"email",
"roles",
"groups",
]
for claim in claim_keys:
if claim in payload:
claims[claim] = payload[claim]
if claims:
logger.debug(
"Extracted %d Azure claims for embedding in FastMCP JWT",
len(claims),
)
return claims
return None
except Exception as e:
logger.debug("Failed to extract Azure claims: %s", e)
return None
async def get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential:
"""Get a cached or new OnBehalfOfCredential for OBO token exchange.
Credentials are cached by user assertion so the Azure SDK's internal
token cache can avoid redundant OBO exchanges when the same user
calls multiple tools with the same scopes.
Args:
user_assertion: The user's access token to exchange via OBO.
Returns:
A configured OnBehalfOfCredential ready for get_token() calls.
Raises:
ImportError: If azure-identity is not installed (requires fastmcp[azure]).
"""
_require_azure_identity("OBO token exchange")
from azure.identity.aio import OnBehalfOfCredential
key = hashlib.sha256(user_assertion.encode()).hexdigest()
if key in self._obo_credentials:
self._obo_credentials.move_to_end(key)
return self._obo_credentials[key]
obo_kwargs: dict[str, Any] = {
"tenant_id": self._tenant_id,
"client_id": self._upstream_client_id,
"user_assertion": user_assertion,
"authority": f"https://{self._base_authority}",
}
if self._upstream_client_secret is not None:
obo_kwargs["client_secret"] = (
self._upstream_client_secret.get_secret_value()
)
else:
raise ValueError(
"OBO token exchange requires either a client_secret or a subclass "
"that overrides get_obo_credential() to provide alternative credentials "
"(e.g., client_assertion_func for managed identity)."
)
credential = OnBehalfOfCredential(**obo_kwargs)
self._obo_credentials[key] = credential
# Evict oldest if over capacity
while len(self._obo_credentials) > self._obo_max_credentials:
_, evicted = self._obo_credentials.popitem(last=False)
await evicted.close()
return credential
async def close_obo_credentials(self) -> None:
"""Close all cached OBO credentials."""
credentials = list(self._obo_credentials.values())
self._obo_credentials.clear()
for credential in credentials:
try:
await credential.close()
except Exception:
logger.debug("Error closing OBO credential", exc_info=True)
class AzureJWTVerifier(JWTVerifier):
"""JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
Auto-configures JWKS URI, issuer, audience, and scope handling from your
Azure app registration details. Designed for Managed Identity and other
token-verification-only scenarios where AzureProvider's full OAuth proxy
isn't needed.
Handles Azure's scope format automatically:
- Validates tokens using short-form scopes (what Azure puts in ``scp`` claims)
- Advertises full-URI scopes in OAuth metadata (what clients need to request)
Example::
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.auth.providers.azure import AzureJWTVerifier
from pydantic import AnyHttpUrl
verifier = AzureJWTVerifier(
client_id="your-client-id",
tenant_id="your-tenant-id",
required_scopes=["access_as_user"],
)
auth = RemoteAuthProvider(
token_verifier=verifier,
authorization_servers=[
AnyHttpUrl("https://login.microsoftonline.com/your-tenant-id/v2.0")
],
base_url="https://my-server.com",
)
"""
def __init__(
self,
*,
client_id: str,
tenant_id: str,
required_scopes: list[str] | None = None,
identifier_uri: str | None = None,
base_authority: str = "login.microsoftonline.com",
):
"""Initialize Azure JWT verifier.
Args:
client_id: Azure application (client) ID from your App registration
tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers").
For multi-tenant apps ("organizations" or "consumers"), issuer validation
is skipped since Azure tokens carry the actual tenant GUID as issuer.
required_scopes: Scope names as they appear in Azure Portal under "Expose an API"
(e.g., ["access_as_user", "read"]). These are validated against
the short-form scopes in token ``scp`` claims, and automatically
prefixed with identifier_uri for OAuth metadata.
identifier_uri: Application ID URI (defaults to ``api://{client_id}``).
Used to prefix scopes in OAuth metadata so clients know the full
scope URIs to request from Azure.
base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
For Azure Government, use "login.microsoftonline.us".
"""
self._identifier_uri = identifier_uri or f"api://{client_id}"
# For multi-tenant apps, Azure tokens carry the actual tenant GUID as
# issuer, not the literal "organizations" or "consumers" string. Skip
# issuer validation for these — audience still protects against wrong-app tokens.
multi_tenant_values = {"organizations", "consumers", "common"}
issuer: str | None = (
None
if tenant_id in multi_tenant_values
else f"https://{base_authority}/{tenant_id}/v2.0"
)
super().__init__(
jwks_uri=f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys",
issuer=issuer,
audience=[client_id, self._identifier_uri],
algorithm="RS256",
required_scopes=required_scopes,
)
@property
def scopes_supported(self) -> list[str]:
"""Return scopes with Azure URI prefix for OAuth metadata.
Azure tokens contain short-form scopes (e.g., ``read``) in the ``scp``
claim, but clients must request full URI scopes (e.g.,
``api://client-id/read``) from the Azure authorization endpoint. This
property returns the full-URI form for OAuth metadata while
``required_scopes`` retains the short form for token validation.
"""
if not self.required_scopes:
return []
prefixed = []
for scope in self.required_scopes:
if scope in OIDC_SCOPES or "://" in scope or "/" in scope:
prefixed.append(scope)
else:
prefixed.append(f"{self._identifier_uri}/{scope}")
return prefixed
# --- Dependency injection support ---
# These require fastmcp[azure] extra for azure-identity
def _require_azure_identity(feature: str) -> None:
"""Raise ImportError with install instructions if azure-identity is not available."""
try:
import azure.identity # noqa: F401
except ImportError as e:
raise ImportError(
f"{feature} requires the `azure` extra. "
"Install with: pip install 'fastmcp[azure]'"
) from e
def _find_azure_provider(auth: AuthProvider | None) -> AzureProvider | None:
"""Extract an AzureProvider from an auth provider, unwrapping MultiAuth if needed."""
if isinstance(auth, AzureProvider):
return auth
if isinstance(auth, MultiAuth) and isinstance(auth.server, AzureProvider):
return auth.server
return None
class _EntraOBOToken(Dependency[str]):
"""Dependency that performs OBO token exchange for Microsoft Entra.
Uses azure.identity's OnBehalfOfCredential for async-native OBO,
with automatic token caching and refresh. Credentials are cached on
the AzureProvider so repeated tool calls reuse existing credentials
and benefit from the Azure SDK's internal token cache.
"""
def __init__(self, scopes: list[str]):
self.scopes = scopes
async def __aenter__(self) -> str:
_require_azure_identity("EntraOBOToken")
from fastmcp.server.dependencies import get_access_token, get_server
access_token = get_access_token()
if access_token is None:
raise RuntimeError(
"No access token available. Cannot perform OBO exchange."
)
server = get_server()
azure_provider = _find_azure_provider(server.auth)
if azure_provider is None:
raise RuntimeError(
"EntraOBOToken requires an AzureProvider as the auth provider. "
f"Current provider: {type(server.auth).__name__}"
)
credential = await azure_provider.get_obo_credential(
user_assertion=access_token.token,
)
result = await credential.get_token(*self.scopes)
return result.token
def EntraOBOToken(scopes: list[str]) -> str:
"""Exchange the user's Entra token for a downstream API token via OBO.
This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
allowing your MCP server to call downstream APIs (like Microsoft Graph) on
behalf of the authenticated user.
Args:
scopes: The scopes to request for the downstream API. For Microsoft Graph,
use scopes like ["https://graph.microsoft.com/Mail.Read"] or
["https://graph.microsoft.com/.default"].
Returns:
A dependency that resolves to the downstream API access token string
Raises:
ImportError: If fastmcp[azure] is not installed
RuntimeError: If no access token is available, provider is not Azure,
or OBO exchange fails
Example:
```python
from fastmcp.server.auth.providers.azure import EntraOBOToken
import httpx
@mcp.tool()
async def get_my_emails(
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"])
):
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://graph.microsoft.com/v1.0/me/messages",
headers={"Authorization": f"Bearer {graph_token}"}
)
return resp.json()
```
Note:
For OBO to work, ensure the scopes are included in the AzureProvider's
`additional_authorize_scopes` parameter, and that admin consent has been
granted for those scopes in your Entra app registration.
"""
return cast(str, _EntraOBOToken(scopes))
import warnings
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.azure is deprecated. "
"Import from fastmcp.server.plugins.auth.azure.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
from fastmcp.server.plugins.auth.azure.provider import (
AzureJWTVerifier,
AzureProvider,
EntraOBOToken,
)
__all__ = ["AzureJWTVerifier", "AzureProvider", "EntraOBOToken"]

View file

@ -1,388 +1,23 @@
"""Clerk OAuth provider for FastMCP.
This module provides a complete Clerk OAuth integration that's ready to use
with a Clerk domain, client ID, and client secret. It handles all the complexity
of Clerk's OAuth/OIDC flow, token validation, and user management.
Clerk uses standard OIDC endpoints derived from the instance domain
(e.g., ``https://<instance>.clerk.accounts.dev``). Token verification is
performed via the introspection endpoint (RFC 7662) for security-critical
checks (active status, audience, scopes), followed by the userinfo endpoint
for profile enrichment. Userinfo failure is non-fatal.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.clerk import ClerkProvider
auth = ClerkProvider(
domain="saving-primate-16.clerk.accounts.dev",
client_id="your-clerk-client-id",
client_secret="your-clerk-client-secret",
base_url="https://my-server.com",
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
"""Backward compatibility shim for Clerk auth provider."""
from __future__ import annotations
import contextlib
from typing import Literal
import warnings
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.clerk is deprecated. "
"Import from fastmcp.server.plugins.auth.clerk.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.clerk.provider import (
ClerkProvider,
ClerkTokenVerifier,
)
class ClerkTokenVerifier(TokenVerifier):
"""Token verifier for Clerk OAuth tokens.
Clerk issues standard OIDC tokens. Verification uses the introspection
endpoint (RFC 7662) as the primary security gate it confirms the token
is active and provides metadata (scopes, expiry, audience). The userinfo
endpoint is called second for profile enrichment (name, email, picture)
and its failure is non-fatal.
When a ``client_id`` is configured, the audience from introspection is
validated against it. When ``required_scopes`` are configured,
introspection must return the token's scopes — the verifier will not
assume scopes when introspection is unavailable.
"""
def __init__(
self,
*,
domain: str,
client_id: str | None = None,
client_secret: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Clerk token verifier.
Args:
domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev")
client_id: Clerk OAuth client ID, used for introspection endpoint authentication
client_secret: Clerk OAuth client secret, used for introspection endpoint authentication
required_scopes: Required OAuth scopes (e.g., ["openid", "email", "profile"])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.domain = domain.rstrip("/")
self._client_id = client_id
self._client_secret = client_secret
self.timeout_seconds = timeout_seconds
self._http_client = http_client
self._userinfo_url = f"https://{self.domain}/oauth/userinfo"
self._introspection_url = f"https://{self.domain}/oauth/token_info"
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify a Clerk OAuth token via introspection and userinfo.
Calls the introspection endpoint first to validate the token and
retrieve auth metadata (active status, scopes, expiry, audience).
If the token passes security checks, the userinfo endpoint is called
for profile enrichment. Userinfo failure is non-fatal.
When a ``client_id`` is configured, the token's audience must match it.
When ``required_scopes`` are configured, introspection must confirm
them; tokens are rejected if scope information is unavailable.
"""
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Step 1: Validate token via introspection (RFC 7662).
# Security-critical checks (active, audience, scopes) come first.
introspect_data_payload: dict = {"token": token}
introspect_kwargs: dict = {
"data": introspect_data_payload,
"headers": {"User-Agent": "FastMCP-Clerk-OAuth"},
}
if self._client_id and self._client_secret:
introspect_kwargs["auth"] = (
self._client_id,
self._client_secret,
)
elif self._client_id:
introspect_data_payload["client_id"] = self._client_id
introspect_response = await client.post(
self._introspection_url,
**introspect_kwargs,
)
if introspect_response.status_code != 200:
logger.debug(
"Clerk introspection failed: %d",
introspect_response.status_code,
)
return None
introspect_data = introspect_response.json()
# RFC 7662 requires the 'active' field in the response.
# A missing field indicates a malformed response — reject.
if "active" not in introspect_data or not introspect_data["active"]:
logger.debug(
"Clerk introspection: token inactive or missing 'active' field"
)
return None
scope_str = introspect_data.get("scope", "")
token_scopes = scope_str.split() if scope_str else []
aud = introspect_data.get("aud") or introspect_data.get("client_id")
expires_at: int | None = None
exp = introspect_data.get("exp")
if exp is not None:
with contextlib.suppress(ValueError, TypeError):
expires_at = int(exp)
if self._client_id and aud != self._client_id:
logger.debug(
"Clerk token audience mismatch: got %s, expected %s",
aud,
self._client_id,
)
return None
if self.required_scopes:
if not token_scopes:
logger.debug(
"Clerk token missing scope information; "
"cannot verify required scopes %s",
self.required_scopes,
)
return None
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"Clerk token missing required scopes. Has %s, needs %s",
token_scopes_set,
required_scopes_set,
)
return None
# Step 2: Fetch user profile via userinfo.
# Enriches the token with profile data (name, email, picture).
sub = introspect_data.get("sub")
user_data: dict = {}
try:
userinfo_response = await client.get(
self._userinfo_url,
headers={
"Authorization": f"Bearer {token}",
"User-Agent": "FastMCP-Clerk-OAuth",
},
)
if userinfo_response.status_code == 200:
user_data = userinfo_response.json()
if not sub:
sub = user_data.get("sub")
except Exception as e:
logger.debug("Clerk userinfo call failed: %s", e)
if not sub:
logger.debug("Clerk token missing 'sub' claim")
return None
access_token = AccessToken(
token=token,
client_id=aud or sub,
scopes=token_scopes,
expires_at=expires_at,
claims={
"sub": sub,
"aud": aud,
"email": user_data.get("email"),
"email_verified": user_data.get("email_verified"),
"name": user_data.get("name"),
"picture": user_data.get("picture"),
"given_name": user_data.get("given_name"),
"family_name": user_data.get("family_name"),
"preferred_username": user_data.get("preferred_username"),
"iss": user_data.get("iss"),
"clerk_user_data": user_data or None,
},
)
logger.debug("Clerk token verified successfully for sub=%s", sub)
return access_token
except httpx.RequestError as e:
logger.debug("Failed to verify Clerk token: %s", e)
return None
except Exception as e:
logger.debug("Clerk token verification error: %s", e)
return None
class ClerkProvider(OAuthProxy):
"""Complete Clerk OAuth provider for FastMCP.
This provider makes it trivial to add Clerk OAuth protection to any
FastMCP server. Provide your Clerk instance domain, OAuth app credentials,
and a base URL, and you're ready to go.
Clerk uses standard OIDC endpoints derived from the instance domain.
All endpoint URLs are constructed automatically from the domain parameter.
Features:
- Transparent OAuth proxy to Clerk
- Automatic token validation via Clerk's userinfo & introspection APIs
- User information extraction from Clerk's OIDC claims
- PKCE support (S256)
- Minimal configuration required
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.clerk import ClerkProvider
auth = ClerkProvider(
domain="saving-primate-16.clerk.accounts.dev",
client_id="your-clerk-client-id",
client_secret="your-clerk-client-secret",
base_url="https://my-server.com",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
domain: str,
client_id: str,
client_secret: str | None = None,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
valid_scopes: list[str] | None = None,
timeout_seconds: int = 10,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
extra_authorize_params: dict[str, str] | None = None,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize Clerk OAuth provider.
Args:
domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev").
This is used to derive all OAuth/OIDC endpoint URLs.
client_id: Clerk OAuth application client ID
client_secret: Clerk OAuth application client secret.
Optional for PKCE public clients. When omitted, jwt_signing_key must be provided.
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Clerk OAuth app (defaults to "/auth/callback")
required_scopes: Required Clerk scopes (defaults to ["openid", "email", "profile"]).
Clerk supports: "openid", "email", "profile", "public_metadata",
"private_metadata", "offline_access".
valid_scopes: All scopes that clients are allowed to request, advertised through
well-known endpoints. Defaults to required_scopes if not provided.
timeout_seconds: HTTP request timeout for Clerk API calls (defaults to 10)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from ``platformdirs``).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes
are provided, they will be used as is. If a string is provided, it will be derived
into a 32-byte key. If not provided, the upstream client secret will be used to
derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing
clients (default True). When "external", the built-in consent screen is skipped
but no warning is logged, indicating that consent is handled externally by Clerk.
consent_csp_policy: Custom CSP policy for the consent page.
extra_authorize_params: Additional parameters to forward to Clerk's authorization
endpoint. Example: {"prompt": "login"} to force re-authentication.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created
per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
domain = domain.rstrip("/")
required_scopes_final = (
parse_scopes(required_scopes)
if required_scopes is not None
else ["openid", "email", "profile"]
)
parsed_valid_scopes = (
parse_scopes(valid_scopes) if valid_scopes is not None else None
)
token_verifier = ClerkTokenVerifier(
domain=domain,
client_id=client_id,
client_secret=client_secret,
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
extra_authorize_params_final = (
dict(extra_authorize_params) if extra_authorize_params else {}
)
super().__init__(
upstream_authorization_endpoint=f"https://{domain}/oauth/authorize",
upstream_token_endpoint=f"https://{domain}/oauth/token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
extra_authorize_params=extra_authorize_params_final or None,
valid_scopes=parsed_valid_scopes,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized Clerk OAuth provider for domain %s with scopes: %s",
domain,
required_scopes_final,
)
__all__ = ["ClerkProvider", "ClerkTokenVerifier"]

View file

@ -1,209 +1,20 @@
"""Descope authentication provider for FastMCP.
This module provides DescopeProvider - a complete authentication solution that integrates
with Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)
for seamless MCP client authentication.
"""
"""Backward compatibility shim for Descope auth provider."""
from __future__ import annotations
from urllib.parse import urlparse
import warnings
import httpx
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.descope is deprecated. "
"Import from fastmcp.server.plugins.auth.descope.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
class DescopeProvider(RemoteAuthProvider):
"""Descope metadata provider for DCR (Dynamic Client Registration).
This provider implements Descope integration using metadata forwarding.
This is the recommended approach for Descope DCR
as it allows Descope to handle the OAuth flow directly while FastMCP acts
as a resource server.
IMPORTANT SETUP REQUIREMENTS:
1. Create an MCP Server in Descope Console:
- Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console
- Create a new MCP Server
- Ensure that **Dynamic Client Registration (DCR)** is enabled
- Note your Well-Known URL
2. Note your Well-Known URL:
- Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)
- Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``
For detailed setup instructions, see:
https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
Example:
```python
from fastmcp.server.auth.providers.descope import DescopeProvider
# Create Descope metadata provider (JWT verifier created automatically)
descope_auth = DescopeProvider(
config_url="https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration",
base_url="https://your-fastmcp-server.com",
)
# Use with FastMCP
mcp = FastMCP("My App", auth=descope_auth)
```
"""
def __init__(
self,
*,
base_url: AnyHttpUrl | str,
config_url: AnyHttpUrl | str | None = None,
project_id: str | None = None,
descope_base_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Descope metadata provider.
Args:
base_url: Public URL of this FastMCP server
config_url: Your Descope Well-Known URL (e.g., "https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration")
This is the new recommended way. If provided, project_id and descope_base_url are ignored.
project_id: Your Descope Project ID (e.g., "P2abc123"). Used with descope_base_url for backwards compatibility.
descope_base_url: Your Descope base URL (e.g., "https://api.descope.com"). Used with project_id for backwards compatibility.
required_scopes: Optional list of scopes that must be present in validated tokens.
These scopes will be included in the protected resource metadata.
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
"""
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
# Parse scopes if provided as string
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else None
)
# Determine which API is being used
if config_url is not None:
# New API: use config_url
# Strip /.well-known/openid-configuration from config_url if present
issuer_url = str(config_url)
if issuer_url.endswith("/.well-known/openid-configuration"):
issuer_url = issuer_url[: -len("/.well-known/openid-configuration")]
# Parse the issuer URL to extract descope_base_url and project_id for other uses
parsed_url = urlparse(issuer_url)
path_parts = parsed_url.path.strip("/").split("/")
# Extract project_id from path (format: /v1/apps/agentic/P.../M...)
if "agentic" in path_parts:
agentic_index = path_parts.index("agentic")
if agentic_index + 1 < len(path_parts):
self.project_id = path_parts[agentic_index + 1]
else:
raise ValueError(
f"Could not extract project_id from config_url: {issuer_url}"
)
else:
raise ValueError(
f"Could not find 'agentic' in config_url path: {issuer_url}"
)
# Extract descope_base_url (scheme + netloc)
self.descope_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}".rstrip(
"/"
)
elif project_id is not None and descope_base_url is not None:
# Old API: use project_id and descope_base_url
self.project_id = project_id
descope_base_url_str = str(descope_base_url).rstrip("/")
# Ensure descope_base_url has a scheme
if not descope_base_url_str.startswith(("http://", "https://")):
descope_base_url_str = f"https://{descope_base_url_str}"
self.descope_base_url = descope_base_url_str
# Old issuer format
issuer_url = f"{self.descope_base_url}/v1/apps/{self.project_id}"
else:
raise ValueError(
"Either config_url (new API) or both project_id and descope_base_url (old API) must be provided"
)
# Create default JWT verifier if none provided
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.descope_base_url}/{self.project_id}/.well-known/jwks.json",
issuer=issuer_url,
algorithm="RS256",
audience=self.project_id,
required_scopes=parsed_scopes,
)
# Initialize RemoteAuthProvider with Descope as the authorization server
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(issuer_url)],
base_url=self.base_url,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get OAuth routes including Descope authorization server metadata forwarding.
This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards Descope's OAuth metadata to clients.
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
# Get the standard protected resource routes from RemoteAuthProvider
routes = super().get_routes(mcp_path)
async def oauth_authorization_server_metadata(request):
"""Forward Descope OAuth authorization server metadata with FastMCP customizations."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.descope_base_url}/v1/apps/{self.project_id}/.well-known/oauth-authorization-server"
)
response.raise_for_status()
metadata = response.json()
return JSONResponse(metadata)
except Exception as e:
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch Descope metadata: {e}",
},
status_code=500,
)
# Add Descope authorization server metadata forwarding
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes
__all__ = ["DescopeProvider"]

View file

@ -1,288 +1,23 @@
"""Discord OAuth provider for FastMCP.
This module provides a complete Discord OAuth integration that's ready to use
with just a client ID and client secret. It handles all the complexity of
Discord's OAuth flow, token validation, and user management.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.discord import DiscordProvider
# Simple Discord OAuth protection
auth = DiscordProvider(
client_id="your-discord-client-id",
client_secret="your-discord-client-secret"
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
"""Backward compatibility shim for Discord auth provider."""
from __future__ import annotations
import contextlib
import time
from datetime import datetime
from typing import Literal
import warnings
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.discord is deprecated. "
"Import from fastmcp.server.plugins.auth.discord.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.discord.provider import (
DiscordProvider,
DiscordTokenVerifier,
)
class DiscordTokenVerifier(TokenVerifier):
"""Token verifier for Discord OAuth tokens.
Discord OAuth tokens are opaque (not JWTs), so we verify them
by calling Discord's tokeninfo API to check if they're valid and get user info.
"""
def __init__(
self,
*,
expected_client_id: str,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Discord token verifier.
Args:
expected_client_id: Expected Discord OAuth client ID for audience binding
required_scopes: Required OAuth scopes (e.g., ['email'])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.expected_client_id = expected_client_id
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify Discord OAuth token by calling Discord's tokeninfo API."""
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Use Discord's tokeninfo endpoint to validate the token
headers = {
"Authorization": f"Bearer {token}",
"User-Agent": "FastMCP-Discord-OAuth",
}
response = await client.get(
"https://discord.com/api/oauth2/@me",
headers=headers,
)
if response.status_code != 200:
logger.debug(
"Discord token verification failed: %d",
response.status_code,
)
return None
token_info = response.json()
# Check if token is expired (Discord returns ISO timestamp)
expires_str = token_info.get("expires")
expires_at = None
if expires_str:
expires_dt = datetime.fromisoformat(
expires_str.replace("Z", "+00:00")
)
expires_at = int(expires_dt.timestamp())
if expires_at <= int(time.time()):
logger.debug("Discord token has expired")
return None
token_scopes = token_info.get("scopes", [])
# Check required scopes
if self.required_scopes:
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"Discord token missing required scopes. Has %d, needs %d",
len(token_scopes_set),
len(required_scopes_set),
)
return None
user_data = token_info.get("user", {})
application = token_info.get("application") or {}
client_id = str(application.get("id", "unknown"))
if client_id != self.expected_client_id:
logger.debug(
"Discord token app ID mismatch: expected %s, got %s",
self.expected_client_id,
client_id,
)
return None
# Create AccessToken with Discord user info
access_token = AccessToken(
token=token,
client_id=client_id,
scopes=token_scopes,
expires_at=expires_at,
claims={
"sub": user_data.get("id"),
"username": user_data.get("username"),
"discriminator": user_data.get("discriminator"),
"avatar": user_data.get("avatar"),
"email": user_data.get("email"),
"verified": user_data.get("verified"),
"locale": user_data.get("locale"),
"discord_user": user_data,
"discord_token_info": token_info,
},
)
logger.debug("Discord token verified successfully")
return access_token
except httpx.RequestError as e:
logger.debug("Failed to verify Discord token: %s", e)
return None
except Exception as e:
logger.debug("Discord token verification error: %s", e)
return None
class DiscordProvider(OAuthProxy):
"""Complete Discord OAuth provider for FastMCP.
This provider makes it trivial to add Discord OAuth protection to any
FastMCP server. Just provide your Discord OAuth app credentials and
a base URL, and you're ready to go.
Features:
- Transparent OAuth proxy to Discord
- Automatic token validation via Discord's API
- User information extraction from Discord APIs
- Minimal configuration required
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.discord import DiscordProvider
auth = DiscordProvider(
client_id="123456789",
client_secret="discord-client-secret-abc123...",
base_url="https://my-server.com"
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize Discord OAuth provider.
Args:
client_id: Discord OAuth client ID (e.g., "123456789")
client_secret: Discord OAuth client secret (e.g., "S....")
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Discord OAuth app (defaults to "/auth/callback")
required_scopes: Required Discord scopes (defaults to ["identify"]). Common scopes include:
- "identify" for profile info (default)
- "email" for email access
- "guilds" for server membership info
timeout_seconds: HTTP request timeout for Discord API calls (defaults to 10)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Discord.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes)
if required_scopes is not None
else ["identify"]
)
# Create Discord token verifier
token_verifier = DiscordTokenVerifier(
expected_client_id=client_id,
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Initialize OAuth proxy with Discord endpoints
super().__init__(
upstream_authorization_endpoint="https://discord.com/oauth2/authorize",
upstream_token_endpoint="https://discord.com/api/oauth2/token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized Discord OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)
__all__ = ["DiscordProvider", "DiscordTokenVerifier"]

View file

@ -1,303 +1,23 @@
"""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)
```
"""
"""Backward compatibility shim for GitHub auth provider."""
from __future__ import annotations
import contextlib
from typing import Literal
import warnings
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.token_cache import TokenCache
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.github is deprecated. "
"Import from fastmcp.server.plugins.auth.github.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.github.provider import (
GitHubProvider,
GitHubTokenVerifier,
)
class GitHubTokenVerifier(TokenVerifier):
"""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.
Caching is disabled by default. Set ``cache_ttl_seconds`` to a positive
integer to cache successful verification results and avoid repeated
GitHub API calls for the same token.
"""
def __init__(
self,
*,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
cache_ttl_seconds: int | None = None,
max_cache_size: int | None = None,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the GitHub token verifier.
Args:
required_scopes: Required OAuth scopes (e.g., ['user:email'])
timeout_seconds: HTTP request timeout
cache_ttl_seconds: How long to cache verification results in seconds.
Caching is disabled by default (None). Set to a positive integer
to enable (e.g., 300 for 5 minutes).
max_cache_size: Maximum number of tokens to cache. Default: 10 000.
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.timeout_seconds = timeout_seconds
self._http_client = http_client
self._cache = TokenCache(
ttl_seconds=cache_ttl_seconds,
max_size=max_cache_size,
)
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify GitHub OAuth token by calling GitHub API."""
is_cached, cached_result = self._cache.get(token)
if is_cached:
logger.debug("GitHub token cache hit")
return cached_result
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Get token info from GitHub API
response = await client.get(
"https://api.github.com/user",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "FastMCP-GitHub-OAuth",
},
)
if response.status_code != 200:
logger.debug(
"GitHub token verification failed: %d - %s",
response.status_code,
response.text[:200],
)
return None
user_data = response.json()
# Get token scopes from GitHub API
# GitHub includes scopes in the X-OAuth-Scopes header
scopes_response = await client.get(
"https://api.github.com/user/repos", # Any authenticated endpoint
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "FastMCP-GitHub-OAuth",
},
)
# Extract scopes from X-OAuth-Scopes header if available
scopes_verified = scopes_response.status_code == 200
oauth_scopes_header = scopes_response.headers.get("x-oauth-scopes", "")
token_scopes = [
scope.strip()
for scope in oauth_scopes_header.split(",")
if scope.strip()
]
# If no scopes in header, assume basic scopes based on successful user API call
if not token_scopes:
token_scopes = ["user"] # Basic scope if we can access user info
# Check required scopes
if self.required_scopes:
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"GitHub token missing required scopes. Has %d, needs %d",
len(token_scopes_set),
len(required_scopes_set),
)
return None
# Create AccessToken with GitHub user info
result = AccessToken(
token=token,
client_id=str(user_data.get("id", "unknown")), # Use GitHub user ID
scopes=token_scopes,
expires_at=None, # GitHub tokens don't typically expire
claims={
"sub": str(user_data["id"]),
"login": user_data.get("login"),
"name": user_data.get("name"),
"email": user_data.get("email"),
"avatar_url": user_data.get("avatar_url"),
"github_user_data": user_data,
},
)
if scopes_verified:
self._cache.set(token, result)
return result
except httpx.RequestError as e:
logger.debug("Failed to verify GitHub token: %s", e)
return None
except Exception as e:
logger.debug("GitHub token verification error: %s", e)
return None
class GitHubProvider(OAuthProxy):
"""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
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
auth = GitHubProvider(
client_id="Ov23li...",
client_secret="abc123...",
base_url="https://my-server.com"
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
cache_ttl_seconds: int | None = None,
max_cache_size: int | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize GitHub OAuth provider.
Args:
client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
client_secret: GitHub OAuth app client secret
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
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 (defaults to 10)
cache_ttl_seconds: How long to cache token verification results in seconds.
Caching is disabled by default (None). Set to a positive integer to
enable (e.g., 300 for 5 minutes).
max_cache_size: Maximum number of tokens to cache. Default: 10 000.
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to GitHub.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["user"]
)
# Create GitHub token verifier
token_verifier = GitHubTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
cache_ttl_seconds=cache_ttl_seconds,
max_cache_size=max_cache_size,
http_client=http_client,
)
# Initialize OAuth proxy with GitHub endpoints
super().__init__(
upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
upstream_token_endpoint="https://github.com/login/oauth/access_token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized GitHub OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)
__all__ = ["GitHubProvider", "GitHubTokenVerifier"]

View file

@ -1,365 +1,23 @@
"""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)
```
"""
"""Backward compatibility shim for Google auth provider."""
from __future__ import annotations
import contextlib
import time
from typing import Literal
import warnings
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.google is deprecated. "
"Import from fastmcp.server.plugins.auth.google.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.google.provider import (
GoogleProvider,
GoogleTokenVerifier,
)
GOOGLE_SCOPE_ALIASES: dict[str, str] = {
"email": "https://www.googleapis.com/auth/userinfo.email",
"profile": "https://www.googleapis.com/auth/userinfo.profile",
}
def _normalize_google_scope(scope: str) -> str:
"""Normalize a Google scope shorthand to its canonical full URI.
Google accepts shorthand scopes like "email" and "profile" in authorization
requests, but returns the full URI form in token responses. This normalizes
to the full URI so comparisons work regardless of which form was used.
"""
return GOOGLE_SCOPE_ALIASES.get(scope, scope)
class GoogleTokenVerifier(TokenVerifier):
"""Token verifier for Google OAuth tokens.
Google OAuth tokens are opaque (not JWTs), so we verify them by calling
Google's tokeninfo endpoint with the access token as a query parameter.
This returns the OAuth app ID (``aud``), granted scopes, and expiry time.
User profile data (name, picture, etc.) is fetched separately from the
v2 userinfo endpoint when the token is valid.
"""
def __init__(
self,
*,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Google token verifier.
Args:
required_scopes: Required OAuth scopes (e.g., ['openid', 'https://www.googleapis.com/auth/userinfo.email'])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
normalized = (
[_normalize_google_scope(s) for s in required_scopes]
if required_scopes
else required_scopes
)
super().__init__(required_scopes=normalized)
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify a Google OAuth token using the tokeninfo endpoint.
Calls ``https://oauth2.googleapis.com/tokeninfo?access_token=TOKEN``
to validate the token and retrieve the OAuth app ID (``aud``), granted
scopes, and expiry time. On success, fetches user profile data from
the v2 userinfo endpoint to populate name, picture, and locale claims.
"""
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Step 1: Verify token via tokeninfo endpoint.
# Returns aud (OAuth app ID), scope (space-separated), expires_in, sub, email.
response = await client.get(
"https://oauth2.googleapis.com/tokeninfo",
params={"access_token": token},
headers={"User-Agent": "FastMCP-Google-OAuth"},
)
if response.status_code != 200:
logger.debug(
"Google token verification failed: %d",
response.status_code,
)
return None
token_data = response.json()
# aud is the OAuth app ID (client_id / audience)
aud = token_data.get("aud")
if not aud:
logger.debug("Google tokeninfo missing 'aud' claim")
return None
# sub is required (unique Google user ID)
sub = token_data.get("sub")
if not sub:
logger.debug("Google tokeninfo missing 'sub' claim")
return None
# Parse scopes directly from the tokeninfo response (space-separated)
scope_str = token_data.get("scope", "")
token_scopes = scope_str.split() if scope_str else []
# Check required scopes
if self.required_scopes:
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"Google token missing required scopes. Has %d, needs %d",
len(token_scopes_set),
len(required_scopes_set),
)
return None
# Compute expiry from expires_in (seconds until expiry)
expires_at: int | None = None
expires_in = token_data.get("expires_in")
if expires_in is not None:
with contextlib.suppress(ValueError, TypeError):
expires_at = int(time.time()) + int(expires_in)
# Step 2: Fetch user profile from v2 userinfo endpoint.
# tokeninfo provides auth data; userinfo provides name, picture, locale.
user_data: dict = {}
try:
userinfo_response = await client.get(
"https://www.googleapis.com/oauth2/v2/userinfo",
headers={
"Authorization": f"Bearer {token}",
"User-Agent": "FastMCP-Google-OAuth",
},
)
if userinfo_response.status_code == 200:
user_data = userinfo_response.json()
except Exception as e:
logger.debug("Failed to fetch Google user profile: %s", e)
access_token = AccessToken(
token=token,
client_id=sub,
scopes=token_scopes,
expires_at=expires_at,
claims={
"sub": sub,
"aud": aud,
"email": token_data.get("email") or user_data.get("email"),
"email_verified": token_data.get("email_verified")
or user_data.get("verified_email"),
"name": user_data.get("name"),
"picture": user_data.get("picture"),
"given_name": user_data.get("given_name"),
"family_name": user_data.get("family_name"),
"locale": user_data.get("locale"),
"google_user_data": user_data or None,
},
)
logger.debug("Google token verified successfully")
return access_token
except httpx.RequestError as e:
logger.debug("Failed to verify Google token: %s", e)
return None
except Exception as e:
logger.debug("Google token verification error: %s", e)
return None
class GoogleProvider(OAuthProxy):
"""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
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
auth = GoogleProvider(
client_id="123456789.apps.googleusercontent.com",
client_secret="GOCSPX-abc123...",
base_url="https://my-server.com"
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str | None = None,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
valid_scopes: list[str] | None = None,
timeout_seconds: int = 10,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
extra_authorize_params: dict[str, str] | None = None,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize Google OAuth provider.
Args:
client_id: Google OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
client_secret: Google OAuth client secret (e.g., "GOCSPX-abc123...").
Optional for PKCE public clients (e.g., native apps). When omitted,
jwt_signing_key must be provided.
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
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
Google scope shorthands like "email" and "profile" are automatically
normalized to their full URI forms for token verification.
valid_scopes: All scopes that clients are allowed to request, advertised through
well-known endpoints. Defaults to required_scopes if not provided. Use this
when you want clients to be able to request additional scopes beyond the
required minimum. Shorthands are normalized to full URI forms.
timeout_seconds: HTTP request timeout for Google API calls (defaults to 10)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Google.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by Google's own consent).
SECURITY WARNING: Only set to False for local development or testing environments.
extra_authorize_params: Additional parameters to forward to Google's authorization endpoint.
By default, GoogleProvider sets {"access_type": "offline", "prompt": "consent"} to ensure
refresh tokens are returned. You can override these defaults or add additional parameters.
Example: {"prompt": "select_account"} to let users choose their Google account.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
# Google requires at least one scope - openid is the minimal OIDC scope
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
# Normalize valid_scopes if provided
parsed_valid_scopes = (
parse_scopes(valid_scopes) if valid_scopes is not None else None
)
valid_scopes_final = (
[_normalize_google_scope(s) for s in parsed_valid_scopes]
if parsed_valid_scopes is not None
else None
)
# Create Google token verifier
# Normalization of shorthand scopes (e.g. "email" -> full URI) happens
# inside GoogleTokenVerifier so required_scopes match what Google returns.
token_verifier = GoogleTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Set Google-specific defaults for extra authorize params
# access_type=offline ensures refresh tokens are returned
# prompt=consent forces consent screen to get refresh token (Google only issues on first auth otherwise)
google_defaults = {
"access_type": "offline",
"prompt": "consent",
}
# User-provided params override defaults
if extra_authorize_params:
google_defaults.update(extra_authorize_params)
extra_authorize_params_final = google_defaults
# Initialize OAuth proxy with Google endpoints
super().__init__(
upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
upstream_token_endpoint="https://oauth2.googleapis.com/token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
extra_authorize_params=extra_authorize_params_final,
valid_scopes=valid_scopes_final,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized Google OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)
__all__ = ["GoogleProvider", "GoogleTokenVerifier"]

View file

@ -1,74 +1,22 @@
"""Keycloak authentication provider for FastMCP."""
"""Backward compatibility shim for Keycloak auth provider."""
from __future__ import annotations
from pydantic import AnyHttpUrl
import warnings
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
logger = get_logger(__name__)
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.keycloak is deprecated. "
"Import from fastmcp.server.plugins.auth.keycloak.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
from fastmcp.server.plugins.auth.keycloak.provider import (
KeycloakAuthProvider,
)
class KeycloakAuthProvider(RemoteAuthProvider):
"""Keycloak authentication provider using Dynamic Client Registration (DCR).
Requires Keycloak 26.6.0 or later, which includes the fix for DCR compatibility
with MCP clients (https://github.com/keycloak/keycloak/pull/45309).
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
auth = KeycloakAuthProvider(
realm_url="https://keycloak.example.com/realms/myrealm",
base_url="https://my-mcp-server.example.com",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
realm_url: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
required_scopes: list[str] | str | None = None,
audience: str | list[str] | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize the Keycloak auth provider.
Args:
realm_url: Keycloak realm URL (e.g., "https://keycloak.example.com/realms/myrealm")
base_url: Public URL of this FastMCP server
required_scopes: Scopes to require on incoming tokens. Defaults to
["openid"], which ensures the `sub` claim (user identifier) is
present in the access token. Override to require additional scopes.
audience: Optional audience(s) for JWT validation. Recommended for production.
token_verifier: Optional custom token verifier. Defaults to a JWTVerifier
configured for Keycloak's JWKS endpoint and issuer.
"""
self.realm_url = str(realm_url).rstrip("/")
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.realm_url}/protocol/openid-connect/certs",
issuer=self.realm_url,
algorithm="RS256",
required_scopes=parsed_scopes,
audience=audience,
)
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(self.realm_url)],
base_url=AnyHttpUrl(str(base_url).rstrip("/")),
)
__all__ = ["KeycloakAuthProvider"]

View file

@ -1,180 +1,20 @@
"""OCI OIDC provider for FastMCP.
"""Backward compatibility shim for OCI auth provider."""
The pull request for the provider is submitted to fastmcp.
from __future__ import annotations
This module provides OIDC Implementation to integrate MCP servers with OCI.
You only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.
import warnings
Post Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.
You need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.
The sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.
You can use the signer object to create OCI service object.
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.oci import OCIProvider
from fastmcp.server.dependencies import get_access_token
from fastmcp.utilities.logging import get_logger
import os
import oci
from oci.auth.signers import TokenExchangeSigner
logger = get_logger(__name__)
# Load configuration from environment
config_url = os.environ.get("OCI_CONFIG_URL") # OCI IAM Domain OIDC discovery URL
client_id = os.environ.get("OCI_CLIENT_ID") # Client ID configured for the OCI IAM Domain Integrated Application
client_secret = os.environ.get("OCI_CLIENT_SECRET") # Client secret configured for the OCI IAM Domain Integrated Application
iam_guid = os.environ.get("OCI_IAM_GUID") # IAM GUID configured for the OCI IAM Domain
# Simple OCI OIDC protection
auth = OCIProvider(
config_url=config_url, # config URL is the OCI IAM Domain OIDC discovery URL
client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
required_scopes=["openid", "profile", "email"],
redirect_path="/auth/callback",
base_url="http://localhost:8000",
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.oci is deprecated. "
"Import from fastmcp.server.plugins.auth.oci.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
# NOTE: For production use, replace this with a thread-safe cache implementation
# such as threading.Lock-protected dict or a proper caching library
_global_token_cache = {} # In memory cache for OCI session token signer
from fastmcp.server.plugins.auth.oci.provider import OCIProvider
def get_oci_signer() -> TokenExchangeSigner:
authntoken = get_access_token()
tokenID = authntoken.claims.get("jti")
token = authntoken.token
# Check if the signer exists for the token ID in memory cache
cached_signer = _global_token_cache.get(tokenID)
logger.debug(f"Global cached signer: {cached_signer}")
if cached_signer:
logger.debug(f"Using globally cached signer for token ID: {tokenID}")
return cached_signer
# If the signer is not yet created for the token then create new OCI signer object
logger.debug(f"Creating new signer for token ID: {tokenID}")
signer = TokenExchangeSigner(
jwt_or_func=token,
oci_domain_id=iam_guid.split(".")[0] if iam_guid else None, # This is same as IAM GUID configured for the OCI IAM Domain
client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
)
logger.debug(f"Signer {signer} created for token ID: {tokenID}")
#Cache the signer object in memory cache
_global_token_cache[tokenID] = signer
logger.debug(f"Signer cached for token ID: {tokenID}")
return signer
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
from typing import Literal
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class OCIProvider(OIDCProxy):
"""An OCI IAM Domain provider implementation for FastMCP.
This provider is a complete OCI integration that's ready to use with
just the configuration URL, client ID, client secret, and base URL.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.oci import OCIProvider
import os
# Load configuration from environment
auth = OCIProvider(
config_url=os.environ.get("OCI_CONFIG_URL"), # OCI IAM Domain OIDC discovery URL
client_id=os.environ.get("OCI_CLIENT_ID"), # Client ID configured for the OCI IAM Domain Integrated Application
client_secret=os.environ.get("OCI_CLIENT_SECRET"), # Client secret configured for the OCI IAM Domain Integrated Application
base_url="http://localhost:8000",
required_scopes=["openid", "profile", "email"],
redirect_path="/auth/callback",
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
def __init__(
self,
*,
config_url: AnyHttpUrl | str,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
audience: str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
redirect_path: str | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
) -> None:
"""Initialize OCI OIDC provider.
Args:
config_url: OCI OIDC Discovery URL
client_id: OCI IAM Domain Integrated Application client id
client_secret: OCI Integrated Application client secret
base_url: Public URL where OIDC endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
audience: OCI API audience (optional)
issuer_url: Issuer URL for OCI IAM Domain metadata. This will override issuer URL from the discovery URL.
required_scopes: Required OCI scopes (defaults to ["openid"])
redirect_path: Redirect path configured in OCI IAM Domain Integrated Application. The default is "/auth/callback".
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
"""
# Parse scopes if provided as string
oci_required_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
super().__init__(
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
audience=audience,
base_url=base_url,
resource_base_url=resource_base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
required_scopes=oci_required_scopes,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
)
logger.debug(
"Initialized OCI OAuth provider for client %s with scopes: %s",
client_id,
oci_required_scopes,
)
__all__ = ["OCIProvider"]

View file

@ -1,234 +1,23 @@
"""PropelAuth authentication provider for FastMCP.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
auth = PropelAuthProvider(
auth_url="https://auth.yourdomain.com",
introspection_client_id="your-client-id",
introspection_client_secret="your-client-secret",
base_url="https://your-fastmcp-server.com",
required_scopes=["read:user_data"],
)
mcp = FastMCP("My App", auth=auth)
```
"""
"""Backward compatibility shim for PropelAuth auth provider."""
from __future__ import annotations
from typing import TypedDict
import warnings
import httpx
from pydantic import AnyHttpUrl, SecretStr
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth import AccessToken, RemoteAuthProvider
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.propelauth is deprecated. "
"Import from fastmcp.server.plugins.auth.propelauth.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.propelauth.provider import (
PropelAuthProvider,
PropelAuthTokenIntrospectionOverrides,
)
class PropelAuthTokenIntrospectionOverrides(TypedDict, total=False):
timeout_seconds: int
cache_ttl_seconds: int | None
max_cache_size: int | None
http_client: httpx.AsyncClient | None
class PropelAuthProvider(RemoteAuthProvider):
"""PropelAuth resource server provider using OAuth 2.1 token introspection.
This provider validates access tokens via PropelAuth's introspection endpoint
and forwards authorization server metadata for OAuth discovery.
Setup:
1. Enable MCP authentication in the PropelAuth Dashboard
2. Configure scopes on the MCP page
3. Select which redirect URIs to enable by picking which clients you support
4. Generate introspection credentials (Client ID + Client Secret)
For detailed setup instructions, see:
https://docs.propelauth.com/mcp-authentication/overview
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
auth = PropelAuthProvider(
auth_url="https://auth.yourdomain.com",
introspection_client_id="your-client-id",
introspection_client_secret="your-client-secret",
base_url="https://your-fastmcp-server.com",
required_scopes=["read:user_data"],
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
auth_url: AnyHttpUrl | str,
introspection_client_id: str,
introspection_client_secret: str | SecretStr,
base_url: AnyHttpUrl | str,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
resource: AnyHttpUrl | str | None = None,
token_introspection_overrides: (
PropelAuthTokenIntrospectionOverrides | None
) = None,
):
"""Initialize PropelAuth provider.
Args:
auth_url: Your PropelAuth Auth URL (from the Backend Integration page)
introspection_client_id: Introspection Client ID from the PropelAuth Dashboard
introspection_client_secret: Introspection Client Secret from the PropelAuth Dashboard
base_url: Public URL of this FastMCP server
required_scopes: Optional list of scopes that must be present in tokens
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
resource: Optional resource URI (RFC 8707) identifying this MCP server.
Use this when multiple MCP servers share the same PropelAuth
authorization server (e.g. ``resource="https://api.example.com/mcp"``),
so only tokens intended for this MCP server are accepted.
token_introspection_overrides: Optional overrides for the underlying
IntrospectionTokenVerifier (timeout, caching, http_client)
"""
normalized_auth_url = str(auth_url).rstrip("/")
introspection_url = f"{normalized_auth_url}/oauth/2.1/introspect"
authorization_server_url = AnyHttpUrl(f"{normalized_auth_url}/oauth/2.1")
if resource is None:
self._resource = None
logger.debug(
"PropelAuthProvider: no resource configured, audience checking disabled"
)
else:
self._resource = str(resource)
token_verifier = self._create_token_verifier(
introspection_url=introspection_url,
client_id=introspection_client_id,
client_secret=introspection_client_secret,
required_scopes=required_scopes,
introspection_overrides=token_introspection_overrides,
)
self._normalized_auth_url = normalized_auth_url
super().__init__(
token_verifier=token_verifier,
authorization_servers=[authorization_server_url],
base_url=base_url,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get routes for this provider.
Includes the standard routes from the RemoteAuthProvider (protected resource metadata routes (RFC 9728)),
and creates an authorization server metadata route that forwards to PropelAuth's route
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
routes = super().get_routes(mcp_path)
async def oauth_authorization_server_metadata(request):
"""Forward PropelAuth OAuth authorization server metadata"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self._normalized_auth_url}/.well-known/oauth-authorization-server/oauth/2.1"
)
response.raise_for_status()
metadata = response.json()
return JSONResponse(metadata)
except Exception as e:
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch PropelAuth metadata: {e}",
},
status_code=500,
)
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify token and check the ``aud`` claim against the configured resource."""
result = await super().verify_token(token)
if result is None or self._resource is None:
return result
aud = result.claims.get("aud")
if aud != self._resource:
logger.debug(
"PropelAuthProvider: token audience %r does not match resource %s",
aud,
self._resource,
)
return None
return result
def _create_token_verifier(
self,
introspection_url: str,
client_id: str,
client_secret: str | SecretStr,
required_scopes: list[str] | None,
introspection_overrides: PropelAuthTokenIntrospectionOverrides | None,
) -> IntrospectionTokenVerifier:
# Being defensive here, check for only the fields we are expecting
safe_overrides: PropelAuthTokenIntrospectionOverrides = {}
if introspection_overrides is not None:
if "timeout_seconds" in introspection_overrides:
safe_overrides["timeout_seconds"] = introspection_overrides[
"timeout_seconds"
]
if "cache_ttl_seconds" in introspection_overrides:
safe_overrides["cache_ttl_seconds"] = introspection_overrides[
"cache_ttl_seconds"
]
if "max_cache_size" in introspection_overrides:
safe_overrides["max_cache_size"] = introspection_overrides[
"max_cache_size"
]
if "http_client" in introspection_overrides:
safe_overrides["http_client"] = introspection_overrides["http_client"]
return IntrospectionTokenVerifier(
introspection_url=introspection_url,
client_id=client_id,
client_secret=client_secret,
required_scopes=required_scopes,
**safe_overrides,
)
__all__ = ["PropelAuthProvider", "PropelAuthTokenIntrospectionOverrides"]

View file

@ -1,212 +1,20 @@
"""Scalekit authentication provider for FastMCP.
This module provides ScalekitProvider - a complete authentication solution that integrates
with Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server
authentication for seamless MCP client authentication.
"""
"""Backward compatibility shim for Scalekit auth provider."""
from __future__ import annotations
import httpx
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
import warnings
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
logger = get_logger(__name__)
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.scalekit is deprecated. "
"Import from fastmcp.server.plugins.auth.scalekit.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
class ScalekitProvider(RemoteAuthProvider):
"""Scalekit resource server provider for OAuth 2.1 authentication.
This provider implements Scalekit integration using resource server pattern.
FastMCP acts as a protected resource server that validates access tokens issued
by Scalekit's authorization server.
IMPORTANT SETUP REQUIREMENTS:
1. Create an MCP Server in Scalekit Dashboard:
- Go to your [Scalekit Dashboard](https://app.scalekit.com/)
- Navigate to MCP Servers section
- Register a new MCP Server with appropriate scopes
- Ensure the Resource Identifier matches exactly what you configure as MCP URL
- Note the Resource ID
2. Environment Configuration:
- Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
- Set SCALEKIT_RESOURCE_ID from your created resource
- Set BASE_URL to your FastMCP server's public URL
For detailed setup instructions, see:
https://docs.scalekit.com/mcp/overview/
Example:
```python
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
# Create Scalekit resource server provider
scalekit_auth = ScalekitProvider(
environment_url="https://your-env.scalekit.com",
resource_id="sk_resource_...",
base_url="https://your-fastmcp-server.com",
)
# Use with FastMCP
mcp = FastMCP("My App", auth=scalekit_auth)
```
"""
def __init__(
self,
*,
environment_url: AnyHttpUrl | str,
resource_id: str,
base_url: AnyHttpUrl | str | None = None,
mcp_url: AnyHttpUrl | str | None = None,
client_id: str | None = None,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Scalekit resource server provider.
Args:
environment_url: Your Scalekit environment URL (e.g., "https://your-env.scalekit.com")
resource_id: Your Scalekit resource ID
base_url: Public URL of this FastMCP server (or use mcp_url for backwards compatibility)
mcp_url: Deprecated alias for base_url. Will be removed in a future release.
client_id: Deprecated parameter, no longer required. Will be removed in a future release.
required_scopes: Optional list of scopes that must be present in tokens
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit
"""
# Resolve base_url from mcp_url if needed (backwards compatibility)
resolved_base_url = base_url or mcp_url
if not resolved_base_url:
raise ValueError("Either base_url or mcp_url must be provided")
if mcp_url is not None:
logger.warning(
"ScalekitProvider parameter 'mcp_url' is deprecated and will be removed in a future release. "
"Rename it to 'base_url'."
)
if client_id is not None:
logger.warning(
"ScalekitProvider no longer requires 'client_id'. The parameter is accepted only for backward "
"compatibility and will be removed in a future release."
)
self.environment_url = str(environment_url).rstrip("/")
self.resource_id = resource_id
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else []
)
self.required_scopes = parsed_scopes
base_url_value = str(resolved_base_url)
logger.debug(
"Initializing ScalekitProvider: environment_url=%s resource_id=%s base_url=%s required_scopes=%s",
self.environment_url,
self.resource_id,
base_url_value,
self.required_scopes,
)
# Create default JWT verifier if none provided
if token_verifier is None:
logger.debug(
"Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s",
f"{self.environment_url}/keys",
self.environment_url,
self.required_scopes,
)
token_verifier = JWTVerifier(
jwks_uri=f"{self.environment_url}/keys",
issuer=self.environment_url,
algorithm="RS256",
audience=self.resource_id,
required_scopes=self.required_scopes or None,
)
else:
logger.debug("Using custom token verifier for ScalekitProvider")
# Initialize RemoteAuthProvider with Scalekit as the authorization server
super().__init__(
token_verifier=token_verifier,
authorization_servers=[
AnyHttpUrl(f"{self.environment_url}/resources/{self.resource_id}")
],
base_url=base_url_value,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get OAuth routes including Scalekit authorization server metadata forwarding.
This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards Scalekit's OAuth metadata to clients.
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
# Get the standard protected resource routes from RemoteAuthProvider
routes = super().get_routes(mcp_path)
logger.debug(
"Preparing Scalekit metadata routes: mcp_path=%s resource_id=%s",
mcp_path,
self.resource_id,
)
async def oauth_authorization_server_metadata(request):
"""Forward Scalekit OAuth authorization server metadata with FastMCP customizations."""
try:
metadata_url = f"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}"
logger.debug(
"Fetching Scalekit OAuth metadata: metadata_url=%s", metadata_url
)
async with httpx.AsyncClient() as client:
response = await client.get(metadata_url)
response.raise_for_status()
metadata = response.json()
logger.debug(
"Scalekit metadata fetched successfully: metadata_keys=%s",
list(metadata.keys()),
)
return JSONResponse(metadata)
except Exception as e:
logger.error(f"Failed to fetch Scalekit metadata: {e}")
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch Scalekit metadata: {e}",
},
status_code=500,
)
# Add Scalekit authorization server metadata forwarding
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes
__all__ = ["ScalekitProvider"]

View file

@ -1,181 +1,20 @@
"""Supabase authentication provider for FastMCP.
This module provides SupabaseProvider - a complete authentication solution that integrates
with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
for seamless MCP client authentication.
"""
"""Backward compatibility shim for Supabase auth provider."""
from __future__ import annotations
from typing import Literal
import warnings
import httpx
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.supabase is deprecated. "
"Import from fastmcp.server.plugins.auth.supabase.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
class SupabaseProvider(RemoteAuthProvider):
"""Supabase metadata provider for DCR (Dynamic Client Registration).
This provider implements Supabase Auth integration using metadata forwarding.
This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
as a resource server, verifying JWTs issued by Supabase Auth.
IMPORTANT SETUP REQUIREMENTS:
1. Supabase Project Setup:
- Create a Supabase project at https://supabase.com
- Note your project URL (e.g., "https://abc123.supabase.co")
- Configure your JWT algorithm in Supabase Auth settings (RS256 or ES256)
- Asymmetric keys (RS256/ES256) are recommended for production
2. JWT Verification:
- FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json
- JWTs are issued by {project_url}{auth_route}
- Default auth_route is "/auth/v1" (can be customized for self-hosted setups)
- Tokens are cached for up to 10 minutes by Supabase's edge servers
- Algorithm must match your Supabase Auth configuration
3. Authorization:
- Supabase uses Row Level Security (RLS) policies for database authorization
- OAuth-level scopes are an upcoming feature in Supabase Auth
- Both approaches will be supported once scope handling is available
For detailed setup instructions, see:
https://supabase.com/docs/guides/auth/jwts
Example:
```python
from fastmcp.server.auth.providers.supabase import SupabaseProvider
# Create Supabase metadata provider (JWT verifier created automatically)
supabase_auth = SupabaseProvider(
project_url="https://abc123.supabase.co",
base_url="https://your-fastmcp-server.com",
algorithm="ES256", # Match your Supabase Auth configuration
)
# Use with FastMCP
mcp = FastMCP("My App", auth=supabase_auth)
```
"""
def __init__(
self,
*,
project_url: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
auth_route: str = "/auth/v1",
algorithm: Literal["RS256", "ES256"] = "ES256",
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Supabase metadata provider.
Args:
project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
base_url: Public URL of this FastMCP server
auth_route: Supabase Auth route. Defaults to "/auth/v1". Can be customized
for self-hosted Supabase Auth setups using custom routes.
algorithm: JWT signing algorithm (RS256 or ES256). Must match your
Supabase Auth configuration. Defaults to ES256.
required_scopes: Optional list of scopes to require for all requests.
Note: Supabase currently uses RLS policies for authorization. OAuth-level
scopes are an upcoming feature.
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
"""
self.project_url = str(project_url).rstrip("/")
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
self.auth_route = auth_route.strip("/")
# Parse scopes if provided as string
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else None
)
# Create default JWT verifier if none provided
if token_verifier is None:
logger.warning(
"SupabaseProvider cannot validate token audience for the specific resource "
"because Supabase Auth does not support RFC 8707 resource indicators. "
"This may leave the server vulnerable to cross-server token replay."
)
token_verifier = JWTVerifier(
jwks_uri=f"{self.project_url}/{self.auth_route}/.well-known/jwks.json",
issuer=f"{self.project_url}/{self.auth_route}",
algorithm=algorithm,
audience="authenticated",
required_scopes=parsed_scopes,
)
# Initialize RemoteAuthProvider with Supabase as the authorization server
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(f"{self.project_url}/{self.auth_route}")],
base_url=self.base_url,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get OAuth routes including Supabase authorization server metadata forwarding.
This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards Supabase's OAuth metadata to clients.
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
# Get the standard protected resource routes from RemoteAuthProvider
routes = super().get_routes(mcp_path)
async def oauth_authorization_server_metadata(request):
"""Forward Supabase OAuth authorization server metadata with FastMCP customizations."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.project_url}/{self.auth_route}/.well-known/oauth-authorization-server"
)
response.raise_for_status()
metadata = response.json()
return JSONResponse(metadata)
except Exception as e:
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch Supabase metadata: {e}",
},
status_code=500,
)
# Add Supabase authorization server metadata forwarding
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes
__all__ = ["SupabaseProvider"]

View file

@ -1,428 +1,25 @@
"""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.
"""
"""Backward compatibility shim for WorkOS auth providers."""
from __future__ import annotations
import contextlib
from typing import Literal
import warnings
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp import settings
from fastmcp.exceptions import FastMCPDeprecationWarning
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.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
if settings.deprecation_warnings:
warnings.warn(
"fastmcp.server.auth.providers.workos is deprecated. "
"Import from fastmcp.server.plugins.auth.workos.provider or "
"fastmcp.server.plugins.auth.authkit.provider instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
logger = get_logger(__name__)
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
from fastmcp.server.plugins.auth.workos.provider import (
WorkOSProvider,
WorkOSTokenVerifier,
)
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,
http_client: httpx.AsyncClient | None = None,
):
"""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
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.authkit_domain = authkit_domain.rstrip("/")
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify WorkOS OAuth token by calling userinfo endpoint."""
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else 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()
token_scopes = (
parse_scopes(user_data.get("scope") or user_data.get("scopes"))
or []
)
if self.required_scopes and not all(
scope in token_scopes for scope in self.required_scopes
):
logger.debug(
"WorkOS token missing required scopes. required=%s actual=%s",
self.required_scopes,
token_scopes,
)
return None
# Create AccessToken with WorkOS user info
return AccessToken(
token=token,
client_id=str(user_data.get("sub", "unknown")),
scopes=token_scopes,
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
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,
client_secret: str,
authkit_domain: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""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 where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
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 (defaults to 10)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to WorkOS.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Apply defaults and ensure authkit_domain is a full URL
authkit_domain_str = 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("/")
scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else []
)
# Create WorkOS token verifier
token_verifier = WorkOSTokenVerifier(
authkit_domain=authkit_domain_final,
required_scopes=scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# 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=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
client_id,
authkit_domain_final,
)
class AuthKitProvider(RemoteAuthProvider):
"""AuthKit metadata provider for DCR (Dynamic Client Registration).
This provider implements AuthKit integration using metadata forwarding
instead of OAuth proxying. This is the recommended approach for WorkOS DCR
as it allows WorkOS to handle the OAuth flow directly while FastMCP acts
as a resource server.
IMPORTANT SETUP REQUIREMENTS:
1. Enable Dynamic Client Registration in WorkOS Dashboard:
- Go to Applications Configuration
- Toggle "Dynamic Client Registration" to enabled
2. Configure your FastMCP server URL as a callback:
- Add your server URL to the Redirects tab in WorkOS dashboard
- Example: https://your-fastmcp-server.com/oauth2/callback
For detailed setup instructions, see:
https://workos.com/docs/authkit/mcp/integrating/token-verification
Token audience is bound to this server automatically: when the MCP
mount path becomes known (typically at ``http_app()`` construction),
``JWTVerifier.audience`` is set to the resource URL advertised in
``.well-known/oauth-protected-resource``. Enable Resource Indicators
(RFC 8707) in your WorkOS Dashboard and list that same URL AuthKit
will then mint tokens with the matching ``aud`` claim.
Example:
```python
from fastmcp.server.auth.providers.workos import AuthKitProvider
workos_auth = AuthKitProvider(
authkit_domain="https://your-workos-domain.authkit.app",
base_url="https://your-fastmcp-server.com",
)
mcp = FastMCP("My App", auth=workos_auth)
```
"""
def __init__(
self,
*,
authkit_domain: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize AuthKit metadata provider.
Args:
authkit_domain: Your AuthKit domain (e.g., "https://your-app.authkit.app")
base_url: Public URL of this FastMCP server
resource_base_url: Optional public base URL for the protected resource.
When provided, this URL is advertised in protected resource metadata
instead of ``base_url``. Useful when OAuth callbacks and the protected
MCP resource live under different public URLs.
required_scopes: Optional list of scopes to require for all requests
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
token_verifier: Optional token verifier. If provided, it is used as-is and
audience auto-wiring is skipped the caller is responsible for setting
an appropriate ``audience``. If None (default), a ``JWTVerifier`` is
created with audience bound to this server's resource URL.
"""
self.authkit_domain = str(authkit_domain).rstrip("/")
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
# Parse scopes if provided as string
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else None
)
# When no custom verifier is provided, we own the JWTVerifier and can
# bind its audience to our resource URL once set_mcp_path() is called.
self._auto_bind_audience = token_verifier is None
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
issuer=self.authkit_domain,
algorithm="RS256",
required_scopes=parsed_scopes,
)
# Initialize RemoteAuthProvider with AuthKit as the authorization server
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(self.authkit_domain)],
base_url=self.base_url,
resource_base_url=resource_base_url,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def set_mcp_path(self, mcp_path: str | None) -> None:
"""Bind the default verifier's audience to this server's resource URL.
AuthKit with Resource Indicators (RFC 8707) mints tokens whose ``aud``
claim equals the resource URL the client requested which is the URL
we advertise in ``.well-known/oauth-protected-resource``. Binding the
audience here keeps validation in lock-step with what clients are sent.
"""
super().set_mcp_path(mcp_path)
if (
self._auto_bind_audience
and self._resource_url is not None
and isinstance(self.token_verifier, JWTVerifier)
):
resource_url = str(self._resource_url)
self.token_verifier.audience = resource_url
logger.info(
"AuthKit tokens will be validated against aud=%s. "
"Configure this URL as a Resource Indicator in the WorkOS Dashboard.",
resource_url,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get OAuth routes including AuthKit authorization server metadata forwarding.
This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards AuthKit's OAuth metadata to clients.
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
# Get the standard protected resource routes from RemoteAuthProvider
routes = super().get_routes(mcp_path)
async def oauth_authorization_server_metadata(request):
"""Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.authkit_domain}/.well-known/oauth-authorization-server"
)
response.raise_for_status()
metadata = response.json()
return JSONResponse(metadata)
except Exception as e:
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch AuthKit metadata: {e}",
},
status_code=500,
)
# Add AuthKit authorization server metadata forwarding
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes
__all__ = ["AuthKitProvider", "WorkOSProvider", "WorkOSTokenVerifier"]

View file

@ -1,67 +1,3 @@
"""Auth plugins for FastMCP."""
"""Auth plugin namespace for FastMCP."""
from fastmcp.server.plugins.auth.providers import (
Auth0Auth,
Auth0AuthConfig,
AuthKitAuth,
AuthKitAuthConfig,
AWSCognitoAuth,
AWSCognitoAuthConfig,
AzureAuth,
AzureAuthConfig,
ClerkAuth,
ClerkAuthConfig,
DescopeAuth,
DescopeAuthConfig,
DiscordAuth,
DiscordAuthConfig,
GitHubAuth,
GitHubAuthConfig,
GoogleAuth,
GoogleAuthConfig,
KeycloakAuth,
KeycloakAuthConfig,
OCIAuth,
OCIAuthConfig,
PropelAuth,
PropelAuthConfig,
ScalekitAuth,
ScalekitAuthConfig,
SupabaseAuth,
SupabaseAuthConfig,
WorkOSAuth,
WorkOSAuthConfig,
)
__all__ = [
"AWSCognitoAuth",
"AWSCognitoAuthConfig",
"Auth0Auth",
"Auth0AuthConfig",
"AuthKitAuth",
"AuthKitAuthConfig",
"AzureAuth",
"AzureAuthConfig",
"ClerkAuth",
"ClerkAuthConfig",
"DescopeAuth",
"DescopeAuthConfig",
"DiscordAuth",
"DiscordAuthConfig",
"GitHubAuth",
"GitHubAuthConfig",
"GoogleAuth",
"GoogleAuthConfig",
"KeycloakAuth",
"KeycloakAuthConfig",
"OCIAuth",
"OCIAuthConfig",
"PropelAuth",
"PropelAuthConfig",
"ScalekitAuth",
"ScalekitAuthConfig",
"SupabaseAuth",
"SupabaseAuthConfig",
"WorkOSAuth",
"WorkOSAuthConfig",
]
__all__: list[str] = []

View file

@ -0,0 +1,65 @@
"""Shared primitives for first-party auth plugins."""
from __future__ import annotations
from typing import Any, Generic, Literal, TypeVar
from pydantic import AnyHttpUrl, BaseModel, ConfigDict
from fastmcp.server.plugins.base import Plugin
ConsentMode = bool | Literal["remember", "external"]
Algorithm = Literal["RS256", "ES256"]
ConfigT = TypeVar("ConfigT", bound=BaseModel)
class AuthPlugin(Plugin[ConfigT], Generic[ConfigT]):
def _require(self, *fields: str) -> None:
missing = [field for field in fields if getattr(self.config, field) is None]
if missing:
names = ", ".join(f"`{field}`" for field in missing)
raise ValueError(f"{type(self).__name__} requires {names}.")
def _require_one(self, *fields: str) -> None:
if not any(getattr(self.config, field) is not None for field in fields):
names = " or ".join(f"`{field}`" for field in fields)
raise ValueError(f"{type(self).__name__} requires {names}.")
def _kwargs(self, *fields: str) -> dict[str, Any]:
return {
field: getattr(self.config, field)
for field in fields
if getattr(self.config, field) is not None
}
class PluginConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
class OAuthProxyConfig(PluginConfig):
base_url: AnyHttpUrl | str | None = None
resource_base_url: AnyHttpUrl | str | None = None
issuer_url: AnyHttpUrl | str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
require_authorization_consent: ConsentMode = True
consent_csp_policy: str | None = None
forward_resource: bool = True
class OAuthProviderConfig(OAuthProxyConfig):
client_id: str | None = None
client_secret: str | None = None
timeout_seconds: int = 10
enable_cimd: bool = True
class RemoteAuthConfig(PluginConfig):
base_url: AnyHttpUrl | str | None = None
required_scopes: list[str] | None = None
scopes_supported: list[str] | None = None
resource_name: str | None = None
resource_documentation: AnyHttpUrl | None = None

View file

@ -0,0 +1,5 @@
"""Auth0 auth plugin."""
from fastmcp.server.plugins.auth.auth0.plugin import Auth0Auth
__all__ = ["Auth0Auth"]

View file

@ -0,0 +1,63 @@
"""Auth0 auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProxyConfig
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
from fastmcp.server.plugins.base import PluginMeta
class Auth0AuthConfig(OAuthProxyConfig):
"""Config model for the Auth0 auth plugin."""
config_url: AnyHttpUrl | str | None = None
client_id: str | None = None
client_secret: str | None = None
audience: str | None = None
class Auth0Auth(AuthPlugin[Auth0AuthConfig]):
"""Contribute an `Auth0Provider` as the server's auth provider."""
Config: ClassVar[type[Auth0AuthConfig]] = Auth0AuthConfig
meta = PluginMeta(name="auth0-auth")
def __init__(
self,
config: Auth0AuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
def auth(self) -> AuthProvider | None:
self._require(
"config_url", "client_id", "client_secret", "audience", "base_url"
)
return Auth0Provider(
**self._kwargs(
"config_url",
"client_id",
"client_secret",
"audience",
"base_url",
"resource_base_url",
"issuer_url",
"required_scopes",
"redirect_path",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
),
client_storage=self._client_storage,
)

View file

@ -0,0 +1,135 @@
"""Auth0 OAuth provider for FastMCP.
This module provides a complete Auth0 integration that's ready to use with
just the configuration URL, client ID, client secret, audience, and base URL.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
# Simple Auth0 OAuth protection
auth = Auth0Provider(
config_url="https://auth0.config.url",
client_id="your-auth0-client-id",
client_secret="your-auth0-client-secret",
audience="your-auth0-api-audience",
base_url="http://localhost:8000",
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
from typing import Literal
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class Auth0Provider(OIDCProxy):
"""An Auth0 provider implementation for FastMCP.
This provider is a complete Auth0 integration that's ready to use with
just the configuration URL, client ID, client secret, audience, and base URL.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
# Simple Auth0 OAuth protection
auth = Auth0Provider(
config_url="https://auth0.config.url",
client_id="your-auth0-client-id",
client_secret="your-auth0-client-secret",
audience="your-auth0-api-audience",
base_url="http://localhost:8000",
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
def __init__(
self,
*,
config_url: AnyHttpUrl | str,
client_id: str,
client_secret: str,
audience: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
redirect_path: str | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
) -> None:
"""Initialize Auth0 OAuth provider.
Args:
config_url: Auth0 config URL
client_id: Auth0 application client id
client_secret: Auth0 application client secret
audience: Auth0 API audience
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
required_scopes: Required Auth0 scopes (defaults to ["openid"])
redirect_path: Redirect path configured in Auth0 application
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Auth0.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
"""
# Parse scopes if provided as string
auth0_required_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
super().__init__(
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
audience=audience,
base_url=base_url,
resource_base_url=resource_base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
required_scopes=auth0_required_scopes,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
)
logger.debug(
"Initialized Auth0 OAuth provider for client %s with scopes: %s",
client_id,
auth0_required_scopes,
)

View file

@ -0,0 +1,5 @@
"""WorkOS AuthKit auth plugin."""
from fastmcp.server.plugins.auth.authkit.plugin import AuthKitAuth
__all__ = ["AuthKitAuth"]

View file

@ -0,0 +1,51 @@
"""WorkOS AuthKit auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider, TokenVerifier
from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
from fastmcp.server.plugins.base import PluginMeta
class AuthKitAuthConfig(RemoteAuthConfig):
"""Config model for the WorkOS AuthKit auth plugin."""
authkit_domain: AnyHttpUrl | str | None = None
resource_base_url: AnyHttpUrl | str | None = None
class AuthKitAuth(AuthPlugin[AuthKitAuthConfig]):
"""Contribute an `AuthKitProvider` as the server's auth provider."""
Config: ClassVar[type[AuthKitAuthConfig]] = AuthKitAuthConfig
meta = PluginMeta(name="authkit-auth")
def __init__(
self,
config: AuthKitAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
self._require("authkit_domain", "base_url")
return AuthKitProvider(
**self._kwargs(
"authkit_domain",
"base_url",
"resource_base_url",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)

View file

@ -0,0 +1,186 @@
"""WorkOS AuthKit provider."""
from __future__ import annotations
import httpx
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class AuthKitProvider(RemoteAuthProvider):
"""AuthKit metadata provider for DCR (Dynamic Client Registration).
This provider implements AuthKit integration using metadata forwarding
instead of OAuth proxying. This is the recommended approach for WorkOS DCR
as it allows WorkOS to handle the OAuth flow directly while FastMCP acts
as a resource server.
IMPORTANT SETUP REQUIREMENTS:
1. Enable Dynamic Client Registration in WorkOS Dashboard:
- Go to Applications -> Configuration
- Toggle "Dynamic Client Registration" to enabled
2. Configure your FastMCP server URL as a callback:
- Add your server URL to the Redirects tab in WorkOS dashboard
- Example: https://your-fastmcp-server.com/oauth2/callback
For detailed setup instructions, see:
https://workos.com/docs/authkit/mcp/integrating/token-verification
Token audience is bound to this server automatically: when the MCP
mount path becomes known (typically at ``http_app()`` construction),
``JWTVerifier.audience`` is set to the resource URL advertised in
``.well-known/oauth-protected-resource``. Enable Resource Indicators
(RFC 8707) in your WorkOS Dashboard and list that same URL AuthKit
will then mint tokens with the matching ``aud`` claim.
Example:
```python
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
workos_auth = AuthKitProvider(
authkit_domain="https://your-workos-domain.authkit.app",
base_url="https://your-fastmcp-server.com",
)
mcp = FastMCP("My App", auth=workos_auth)
```
"""
def __init__(
self,
*,
authkit_domain: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize AuthKit metadata provider.
Args:
authkit_domain: Your AuthKit domain (e.g., "https://your-app.authkit.app")
base_url: Public URL of this FastMCP server
resource_base_url: Optional public base URL for the protected resource.
When provided, this URL is advertised in protected resource metadata
instead of ``base_url``. Useful when OAuth callbacks and the protected
MCP resource live under different public URLs.
required_scopes: Optional list of scopes to require for all requests
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
token_verifier: Optional token verifier. If provided, it is used as-is and
audience auto-wiring is skipped the caller is responsible for setting
an appropriate ``audience``. If None (default), a ``JWTVerifier`` is
created with audience bound to this server's resource URL.
"""
self.authkit_domain = str(authkit_domain).rstrip("/")
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else None
)
# When no custom verifier is provided, we own the JWTVerifier and can
# bind its audience to our resource URL once set_mcp_path() is called.
self._auto_bind_audience = token_verifier is None
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
issuer=self.authkit_domain,
algorithm="RS256",
required_scopes=parsed_scopes,
)
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(self.authkit_domain)],
base_url=self.base_url,
resource_base_url=resource_base_url,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def set_mcp_path(self, mcp_path: str | None) -> None:
"""Bind the default verifier's audience to this server's resource URL.
AuthKit with Resource Indicators (RFC 8707) mints tokens whose ``aud``
claim equals the resource URL the client requested which is the URL
we advertise in ``.well-known/oauth-protected-resource``. Binding the
audience here keeps validation in lock-step with what clients are sent.
"""
super().set_mcp_path(mcp_path)
if (
self._auto_bind_audience
and self._resource_url is not None
and isinstance(self.token_verifier, JWTVerifier)
):
resource_url = str(self._resource_url)
self.token_verifier.audience = resource_url
logger.info(
"AuthKit tokens will be validated against aud=%s. "
"Configure this URL as a Resource Indicator in the WorkOS Dashboard.",
resource_url,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get OAuth routes including AuthKit authorization server metadata forwarding.
This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards AuthKit's OAuth metadata to clients.
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
routes = super().get_routes(mcp_path)
async def oauth_authorization_server_metadata(request):
"""Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.authkit_domain}/.well-known/oauth-authorization-server"
)
response.raise_for_status()
metadata = response.json()
return JSONResponse(metadata)
except Exception as e:
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch AuthKit metadata: {e}",
},
status_code=500,
)
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes
__all__ = ["AuthKitProvider"]

View file

@ -0,0 +1,5 @@
"""AWS Cognito auth plugin."""
from fastmcp.server.plugins.auth.aws.plugin import AWSCognitoAuth
__all__ = ["AWSCognitoAuth"]

View file

@ -0,0 +1,61 @@
"""AWS Cognito auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProxyConfig
from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
from fastmcp.server.plugins.base import PluginMeta
class AWSCognitoAuthConfig(OAuthProxyConfig):
"""Config model for the AWS Cognito auth plugin."""
user_pool_id: str | None = None
client_id: str | None = None
client_secret: str | None = None
aws_region: str = "eu-central-1"
redirect_path: str | None = "/auth/callback"
class AWSCognitoAuth(AuthPlugin[AWSCognitoAuthConfig]):
"""Contribute an `AWSCognitoProvider` as the server's auth provider."""
Config: ClassVar[type[AWSCognitoAuthConfig]] = AWSCognitoAuthConfig
meta = PluginMeta(name="aws-cognito-auth")
def __init__(
self,
config: AWSCognitoAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
def auth(self) -> AuthProvider | None:
self._require("user_pool_id", "client_id", "client_secret", "base_url")
return AWSCognitoProvider(
**self._kwargs(
"user_pool_id",
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"aws_region",
"issuer_url",
"redirect_path",
"required_scopes",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
),
client_storage=self._client_storage,
)

View file

@ -0,0 +1,229 @@
"""AWS Cognito OAuth provider for FastMCP.
This module provides a complete AWS Cognito OAuth integration that's ready to use
with a user pool ID, domain prefix, client ID and client secret. It handles all
the complexity of AWS Cognito's OAuth flow, token validation, and user management.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
# Simple AWS Cognito OAuth protection
auth = AWSCognitoProvider(
user_pool_id="your-user-pool-id",
aws_region="eu-central-1",
client_id="your-cognito-client-id",
client_secret="your-cognito-client-secret"
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
from __future__ import annotations
from typing import Literal
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class AWSCognitoTokenVerifier(JWTVerifier):
"""Token verifier for Cognito access tokens.
Cognito access tokens use a ``client_id`` claim instead of the
standard ``aud`` claim. This subclass passes ``audience=None``
to the parent (skipping the ``aud`` check) and validates the
``client_id`` claim directly.
"""
def __init__(self, *, audience: str | list[str] | None = None, **kwargs):
self._expected_client_id = audience
super().__init__(audience=None, **kwargs)
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify token and filter claims to Cognito-specific subset."""
access_token = await super().verify_token(token)
if not access_token:
return None
# Validate client_id claim (Cognito's equivalent of aud)
if self._expected_client_id:
token_client_id = access_token.claims.get("client_id")
if isinstance(self._expected_client_id, list):
valid = token_client_id in self._expected_client_id
else:
valid = token_client_id == self._expected_client_id
if not valid:
self.logger.debug(
"Token validation failed: client_id mismatch (expected %s, got %s)",
self._expected_client_id,
token_client_id,
)
return None
# Filter claims to Cognito-specific subset
cognito_claims = {
"sub": access_token.claims.get("sub"),
"username": access_token.claims.get("username"),
"cognito:groups": access_token.claims.get("cognito:groups", []),
}
return AccessToken(
token=access_token.token,
client_id=access_token.client_id,
scopes=access_token.scopes,
expires_at=access_token.expires_at,
claims=cognito_claims,
)
class AWSCognitoProvider(OIDCProxy):
"""Complete AWS Cognito OAuth provider for FastMCP.
This provider makes it trivial to add AWS Cognito OAuth protection to any
FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,
client credentials, and a base URL, and you're ready to go.
Features:
- Automatic OIDC Discovery from AWS Cognito User Pool
- Automatic JWT token validation via Cognito's public keys
- Cognito-specific claim filtering (sub, username, cognito:groups)
- Support for Cognito User Pools
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
auth = AWSCognitoProvider(
user_pool_id="eu-central-1_XXXXXXXXX",
aws_region="eu-central-1",
client_id="your-cognito-client-id",
client_secret="your-cognito-client-secret",
base_url="https://my-server.com",
redirect_path="/custom/callback",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
user_pool_id: str,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
aws_region: str = "eu-central-1",
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str = "/auth/callback",
required_scopes: list[str] | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
):
"""Initialize AWS Cognito OAuth provider.
Args:
user_pool_id: Your Cognito User Pool ID (e.g., "eu-central-1_XXXXXXXXX")
client_id: Cognito app client ID
client_secret: Cognito app client secret
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
required_scopes: Required Cognito scopes (defaults to ["openid"])
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to AWS Cognito.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
"""
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
# Construct OIDC discovery URL
config_url = f"https://cognito-idp.{aws_region}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration"
# Store Cognito-specific info for claim filtering
self.user_pool_id = user_pool_id
self.aws_region = aws_region
self.client_id = client_id
# Initialize OIDC proxy with Cognito discovery
super().__init__(
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
algorithm="RS256",
required_scopes=required_scopes_final,
base_url=base_url,
resource_base_url=resource_base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
)
logger.debug(
"Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)
def get_token_verifier(
self,
*,
algorithm: str | None = None,
audience: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int | None = None,
) -> AWSCognitoTokenVerifier:
"""Creates a Cognito-specific token verifier with claim filtering.
Args:
algorithm: Optional token verifier algorithm
audience: Optional token verifier audience
required_scopes: Optional token verifier required_scopes
timeout_seconds: HTTP request timeout in seconds
"""
return AWSCognitoTokenVerifier(
issuer=str(self.oidc_config.issuer),
audience=audience or self.client_id,
algorithm=algorithm,
jwks_uri=str(self.oidc_config.jwks_uri),
required_scopes=required_scopes,
)

View file

@ -0,0 +1,5 @@
"""Azure auth plugin."""
from fastmcp.server.plugins.auth.azure.plugin import AzureAuth
__all__ = ["AzureAuth"]

View file

@ -0,0 +1,69 @@
"""Azure auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
from fastmcp.server.plugins.auth.azure.provider import AzureProvider
from fastmcp.server.plugins.base import PluginMeta
class AzureAuthConfig(OAuthProviderConfig):
"""Config model for the Azure auth plugin."""
tenant_id: str | None = None
required_scopes: list[str] | None = None
identifier_uri: str | None = None
additional_authorize_scopes: list[str] | None = None
base_authority: str = "login.microsoftonline.com"
class AzureAuth(AuthPlugin[AzureAuthConfig]):
"""Contribute an `AzureProvider` as the server's auth provider."""
Config: ClassVar[type[AzureAuthConfig]] = AzureAuthConfig
meta = PluginMeta(name="azure-auth")
def __init__(
self,
config: AzureAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
self._require("client_id", "tenant_id", "required_scopes", "base_url")
self._require_one("client_secret", "jwt_signing_key")
return AzureProvider(
**self._kwargs(
"client_id",
"client_secret",
"tenant_id",
"required_scopes",
"base_url",
"resource_base_url",
"identifier_uri",
"issuer_url",
"redirect_path",
"additional_authorize_scopes",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"base_authority",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)

View file

@ -0,0 +1,768 @@
"""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 hashlib
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Literal, cast
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.dependencies import Dependency
from fastmcp.server.auth.auth import MultiAuth
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from azure.identity.aio import OnBehalfOfCredential
from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyHttpUrl
from fastmcp.server.auth.auth import AuthProvider
logger = get_logger(__name__)
# Standard OIDC scopes that should never be prefixed with identifier_uri.
# Per Microsoft docs: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
# "OIDC scopes are requested as simple string identifiers without resource prefixes"
OIDC_SCOPES = frozenset({"openid", "profile", "email", "offline_access"})
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.
Scope Handling:
- required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
Automatically prefixed with identifier_uri during initialization
Validated on all tokens and advertised to MCP clients
- additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
NOT prefixed, NOT validated, NOT advertised to clients
Used to request Microsoft Graph or other upstream API permissions
Features:
- OAuth proxy to Azure/Microsoft identity platform
- JWT validation using tenant issuer and JWKS
- Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
- Custom API scopes and Microsoft Graph scopes in a single provider
Setup:
1. Create an App registration in Azure Portal
2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
4. Add custom scopes (e.g., "read", "write") under "Expose an API"
5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
6. Create a client secret
7. Get Application (client) ID, Directory (tenant) ID, and client secret
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.azure.provider import AzureProvider
# Standard Azure (Public Cloud)
auth = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
required_scopes=["read", "write"], # Unprefixed scope names
additional_authorize_scopes=["User.Read", "Mail.Read"], # Optional Graph scopes
base_url="http://localhost:8000",
# identifier_uri defaults to api://{client_id}
)
# Azure Government
auth_gov = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
required_scopes=["read", "write"],
base_authority="login.microsoftonline.us", # Override for Azure Gov
base_url="http://localhost:8000",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str | None = None,
tenant_id: str,
required_scopes: list[str],
base_url: str,
resource_base_url: AnyHttpUrl | str | None = None,
identifier_uri: str | None = None,
issuer_url: str | None = None,
redirect_path: str | None = None,
additional_authorize_scopes: list[str] | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
base_authority: str = "login.microsoftonline.com",
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
) -> None:
"""Initialize Azure OAuth provider.
Args:
client_id: Azure application (client) ID from your App registration
client_secret: Azure client secret from your App registration. Optional when
using alternative credentials (e.g., managed identity with a custom
_create_upstream_oauth_client override). When omitted, jwt_signing_key
must be provided.
tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers")
identifier_uri: Optional Application ID URI for your custom API (defaults to api://{client_id}).
This URI is automatically prefixed to all required_scopes during initialization.
Example: identifier_uri="api://my-api" + required_scopes=["read"]
tokens validated for "api://my-api/read"
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
For Azure Government, use "login.microsoftonline.us".
required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
- Automatically prefixed with identifier_uri during initialization
- Validated on all tokens
- Advertised in Protected Resource Metadata
- Must match scope names defined in Azure Portal under "Expose an API"
Example: ["read", "write"] validates tokens containing ["api://xxx/read", "api://xxx/write"]
additional_authorize_scopes: Microsoft Graph or other upstream scopes in full format.
- NOT prefixed with identifier_uri
- NOT validated on tokens
- NOT advertised to MCP clients
- Used to request additional permissions from Azure (e.g., Graph API access)
Example: ["User.Read", "Mail.Read"]
These scopes allow your FastMCP server to call Microsoft Graph APIs using the
upstream Azure token, but MCP clients are unaware of them.
Note: "offline_access" is automatically included to obtain refresh tokens.
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Azure.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in JWKS fetches.
When provided, the client is reused for JWT key fetches and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per fetch.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
parsed_required_scopes = parse_scopes(required_scopes)
parsed_additional_scopes: list[str] = (
parse_scopes(additional_authorize_scopes) or []
if additional_authorize_scopes
else []
)
# Always include offline_access to get refresh tokens from Azure
if "offline_access" not in parsed_additional_scopes:
parsed_additional_scopes = [*parsed_additional_scopes, "offline_access"]
# Store Azure-specific config for OBO credential creation
self._tenant_id = tenant_id
self._base_authority = base_authority
# Cache of OBO credentials keyed by hash of user assertion token.
# Reusing credentials allows the Azure SDK's internal token cache
# to avoid redundant OBO exchanges for the same user + scopes.
self._obo_credentials: OrderedDict[str, OnBehalfOfCredential] = OrderedDict()
self._obo_max_credentials: int = 128
# Apply defaults
self.identifier_uri = identifier_uri or f"api://{client_id}"
self.additional_authorize_scopes: list[str] = parsed_additional_scopes
# Always validate tokens against the app's API client ID using JWT
issuer = f"https://{base_authority}/{tenant_id}/v2.0"
jwks_uri = f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys"
# Azure access tokens only include custom API scopes in the `scp` claim,
# NOT standard OIDC scopes (openid, profile, email, offline_access).
# Filter out OIDC scopes from validation - they'll still be sent to Azure
# during authorization (handled by _prefix_scopes_for_azure).
validation_scopes = [
s for s in (parsed_required_scopes or []) if s not in OIDC_SCOPES
]
if not validation_scopes:
raise ValueError(
"AzureProvider requires at least one non-OIDC scope in "
"required_scopes (e.g., 'read', 'write'). OIDC scopes like "
"'openid', 'profile', 'email', and 'offline_access' are not "
"included in Azure access token claims and cannot be used for "
"scope enforcement."
)
token_verifier = JWTVerifier(
jwks_uri=jwks_uri,
issuer=issuer,
audience=[client_id, self.identifier_uri],
algorithm="RS256",
required_scopes=validation_scopes, # Only validate non-OIDC scopes
http_client=http_client,
)
# Build Azure OAuth endpoints with tenant
authorization_endpoint = (
f"https://{base_authority}/{tenant_id}/oauth2/v2.0/authorize"
)
token_endpoint = f"https://{base_authority}/{tenant_id}/oauth2/v2.0/token"
# Initialize OAuth proxy with Azure endpoints
# Remember there's hooks called, such as _prepare_scopes_for_token_exchange
# and _prepare_scopes_for_upstream_refresh
super().__init__(
upstream_authorization_endpoint=authorization_endpoint,
upstream_token_endpoint=token_endpoint,
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
valid_scopes=parsed_required_scopes,
enable_cimd=enable_cimd,
)
authority_info = ""
if base_authority != "login.microsoftonline.com":
authority_info = f" using authority {base_authority}"
logger.info(
"Initialized Azure OAuth provider for client %s with tenant %s%s%s",
client_id,
tenant_id,
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
authority_info,
)
async def authorize(
self,
client: OAuthClientInformationFull,
params: AuthorizationParams,
) -> str:
"""Start OAuth transaction and redirect to Azure AD.
Override parent's authorize method to filter out the 'resource' parameter
which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
scopes to determine the resource/audience instead of a separate parameter.
Args:
client: OAuth client information
params: Authorization parameters from the client
Returns:
Authorization URL to redirect the user to Azure AD
"""
# Clear the resource parameter that Azure AD v2.0 doesn't support
# This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
# but Azure AD v2.0 uses scopes instead to determine the audience
params_to_use = params
if hasattr(params, "resource"):
original_resource = getattr(params, "resource", None)
if original_resource is not None:
params_to_use = params.model_copy(update={"resource": None})
if original_resource:
logger.debug(
"Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
original_resource,
)
# Don't modify the scopes in params - they stay unprefixed for MCP clients
# We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
auth_url = await super().authorize(client, params_to_use)
separator = "&" if "?" in auth_url else "?"
return f"{auth_url}{separator}prompt=select_account"
def _prefix_scopes_for_azure(self, scopes: list[str]) -> list[str]:
"""Prefix unprefixed custom API scopes with identifier_uri for Azure.
This helper centralizes the scope prefixing logic used in both
authorization and token refresh flows.
Scopes that are NOT prefixed:
- Standard OIDC scopes (openid, profile, email, offline_access)
- Fully-qualified URIs (contain "://")
- Scopes with path component (contain "/")
Note: Microsoft Graph scopes (e.g., User.Read) should be passed via
`additional_authorize_scopes` or use fully-qualified format
(e.g., https://graph.microsoft.com/User.Read).
Args:
scopes: List of scopes, may be prefixed or unprefixed
Returns:
List of scopes with identifier_uri prefix applied where needed
"""
prefixed = []
for scope in scopes:
if scope in OIDC_SCOPES:
# Standard OIDC scopes - never prefix
prefixed.append(scope)
elif "://" in scope or "/" in scope:
# Already fully-qualified (e.g., "api://xxx/read" or
# "https://graph.microsoft.com/User.Read")
prefixed.append(scope)
else:
# Unprefixed custom API scope - prefix with identifier_uri
prefixed.append(f"{self.identifier_uri}/{scope}")
return prefixed
def _build_upstream_authorize_url(
self, txn_id: str, transaction: dict[str, Any]
) -> str:
"""Build Azure authorization URL with prefixed scopes.
Overrides parent to prefix scopes with identifier_uri before sending to Azure,
while keeping unprefixed scopes in the transaction for MCP clients.
"""
# Get unprefixed scopes from transaction
unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
# Prefix scopes for Azure authorization request
prefixed_scopes = self._prefix_scopes_for_azure(unprefixed_scopes)
# Add Microsoft Graph scopes (not validated, not prefixed)
if self.additional_authorize_scopes:
prefixed_scopes.extend(self.additional_authorize_scopes)
# Temporarily modify transaction dict for parent's URL building
modified_transaction = transaction.copy()
modified_transaction["scopes"] = prefixed_scopes
# Let parent build the URL with prefixed scopes
return super()._build_upstream_authorize_url(txn_id, modified_transaction)
def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]:
"""Prepare scopes for Azure authorization code exchange.
Azure requires scopes during token exchange (AADSTS28003 error if missing).
Azure only allows ONE resource per token request (AADSTS28000), so we only
include scopes for this API plus OIDC scopes.
Args:
scopes: Scopes from the authorization request (unprefixed)
Returns:
List of scopes for Azure token endpoint
"""
# Prefix scopes for this API
prefixed_scopes = self._prefix_scopes_for_azure(scopes or [])
# Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
if self.additional_authorize_scopes:
prefixed_scopes.extend(
s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
)
deduplicated = list(dict.fromkeys(prefixed_scopes))
logger.debug("Token exchange scopes: %s", deduplicated)
return deduplicated
def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:
"""Prepare scopes for Azure token refresh.
Azure requires fully-qualified scopes and only allows ONE resource per
token request (AADSTS28000). We include scopes for this API plus OIDC scopes.
Args:
scopes: Base scopes from RefreshToken (unprefixed, e.g., ["read"])
Returns:
Deduplicated list of scopes formatted for Azure token endpoint
"""
logger.debug("Base scopes from storage: %s", scopes)
# Filter out any additional_authorize_scopes that may have been stored
additional_scopes_set = set(self.additional_authorize_scopes or [])
base_scopes = [s for s in scopes if s not in additional_scopes_set]
# Prefix base scopes with identifier_uri for Azure
prefixed_scopes = self._prefix_scopes_for_azure(base_scopes)
# Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
if self.additional_authorize_scopes:
prefixed_scopes.extend(
s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
)
deduplicated_scopes = list(dict.fromkeys(prefixed_scopes))
logger.debug("Scopes for Azure token endpoint: %s", deduplicated_scopes)
return deduplicated_scopes
async def _extract_upstream_claims(
self, idp_tokens: dict[str, Any]
) -> dict[str, Any] | None:
"""Extract claims from Azure token response to embed in FastMCP JWT.
Decodes the Azure access token (which is a JWT) to extract user identity
claims. This allows gateways to inspect upstream identity information by
decoding the FastMCP JWT without needing server-side storage lookups.
Azure access tokens contain claims like:
- sub: Subject identifier (unique per user per application)
- oid: Object ID (unique user identifier across Azure AD)
- tid: Tenant ID
- azp: Authorized party (client ID that requested the token)
- name: Display name
- given_name: First name
- family_name: Last name
- preferred_username: User principal name (email format)
- upn: User Principal Name
- email: Email address (if available)
- roles: Application roles assigned to the user
- groups: Group memberships (if configured)
Args:
idp_tokens: Full token response from Azure, containing access_token
and potentially id_token.
Returns:
Dict of extracted claims, or None if extraction fails.
"""
access_token = idp_tokens.get("access_token")
if not access_token:
return None
try:
# Azure access tokens are JWTs - decode without verification
# (already validated by token_verifier during token exchange)
payload = decode_jwt_payload(access_token)
# Extract useful identity claims
claims: dict[str, Any] = {}
claim_keys = [
"sub",
"oid",
"tid",
"azp",
"name",
"given_name",
"family_name",
"preferred_username",
"upn",
"email",
"roles",
"groups",
]
for claim in claim_keys:
if claim in payload:
claims[claim] = payload[claim]
if claims:
logger.debug(
"Extracted %d Azure claims for embedding in FastMCP JWT",
len(claims),
)
return claims
return None
except Exception as e:
logger.debug("Failed to extract Azure claims: %s", e)
return None
async def get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential:
"""Get a cached or new OnBehalfOfCredential for OBO token exchange.
Credentials are cached by user assertion so the Azure SDK's internal
token cache can avoid redundant OBO exchanges when the same user
calls multiple tools with the same scopes.
Args:
user_assertion: The user's access token to exchange via OBO.
Returns:
A configured OnBehalfOfCredential ready for get_token() calls.
Raises:
ImportError: If azure-identity is not installed (requires fastmcp[azure]).
"""
_require_azure_identity("OBO token exchange")
from azure.identity.aio import OnBehalfOfCredential
key = hashlib.sha256(user_assertion.encode()).hexdigest()
if key in self._obo_credentials:
self._obo_credentials.move_to_end(key)
return self._obo_credentials[key]
obo_kwargs: dict[str, Any] = {
"tenant_id": self._tenant_id,
"client_id": self._upstream_client_id,
"user_assertion": user_assertion,
"authority": f"https://{self._base_authority}",
}
if self._upstream_client_secret is not None:
obo_kwargs["client_secret"] = (
self._upstream_client_secret.get_secret_value()
)
else:
raise ValueError(
"OBO token exchange requires either a client_secret or a subclass "
"that overrides get_obo_credential() to provide alternative credentials "
"(e.g., client_assertion_func for managed identity)."
)
credential = OnBehalfOfCredential(**obo_kwargs)
self._obo_credentials[key] = credential
# Evict oldest if over capacity
while len(self._obo_credentials) > self._obo_max_credentials:
_, evicted = self._obo_credentials.popitem(last=False)
await evicted.close()
return credential
async def close_obo_credentials(self) -> None:
"""Close all cached OBO credentials."""
credentials = list(self._obo_credentials.values())
self._obo_credentials.clear()
for credential in credentials:
try:
await credential.close()
except Exception:
logger.debug("Error closing OBO credential", exc_info=True)
class AzureJWTVerifier(JWTVerifier):
"""JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
Auto-configures JWKS URI, issuer, audience, and scope handling from your
Azure app registration details. Designed for Managed Identity and other
token-verification-only scenarios where AzureProvider's full OAuth proxy
isn't needed.
Handles Azure's scope format automatically:
- Validates tokens using short-form scopes (what Azure puts in ``scp`` claims)
- Advertises full-URI scopes in OAuth metadata (what clients need to request)
Example::
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.plugins.auth.azure.provider import AzureJWTVerifier
from pydantic import AnyHttpUrl
verifier = AzureJWTVerifier(
client_id="your-client-id",
tenant_id="your-tenant-id",
required_scopes=["access_as_user"],
)
auth = RemoteAuthProvider(
token_verifier=verifier,
authorization_servers=[
AnyHttpUrl("https://login.microsoftonline.com/your-tenant-id/v2.0")
],
base_url="https://my-server.com",
)
"""
def __init__(
self,
*,
client_id: str,
tenant_id: str,
required_scopes: list[str] | None = None,
identifier_uri: str | None = None,
base_authority: str = "login.microsoftonline.com",
):
"""Initialize Azure JWT verifier.
Args:
client_id: Azure application (client) ID from your App registration
tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers").
For multi-tenant apps ("organizations" or "consumers"), issuer validation
is skipped since Azure tokens carry the actual tenant GUID as issuer.
required_scopes: Scope names as they appear in Azure Portal under "Expose an API"
(e.g., ["access_as_user", "read"]). These are validated against
the short-form scopes in token ``scp`` claims, and automatically
prefixed with identifier_uri for OAuth metadata.
identifier_uri: Application ID URI (defaults to ``api://{client_id}``).
Used to prefix scopes in OAuth metadata so clients know the full
scope URIs to request from Azure.
base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
For Azure Government, use "login.microsoftonline.us".
"""
self._identifier_uri = identifier_uri or f"api://{client_id}"
# For multi-tenant apps, Azure tokens carry the actual tenant GUID as
# issuer, not the literal "organizations" or "consumers" string. Skip
# issuer validation for these — audience still protects against wrong-app tokens.
multi_tenant_values = {"organizations", "consumers", "common"}
issuer: str | None = (
None
if tenant_id in multi_tenant_values
else f"https://{base_authority}/{tenant_id}/v2.0"
)
super().__init__(
jwks_uri=f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys",
issuer=issuer,
audience=[client_id, self._identifier_uri],
algorithm="RS256",
required_scopes=required_scopes,
)
@property
def scopes_supported(self) -> list[str]:
"""Return scopes with Azure URI prefix for OAuth metadata.
Azure tokens contain short-form scopes (e.g., ``read``) in the ``scp``
claim, but clients must request full URI scopes (e.g.,
``api://client-id/read``) from the Azure authorization endpoint. This
property returns the full-URI form for OAuth metadata while
``required_scopes`` retains the short form for token validation.
"""
if not self.required_scopes:
return []
prefixed = []
for scope in self.required_scopes:
if scope in OIDC_SCOPES or "://" in scope or "/" in scope:
prefixed.append(scope)
else:
prefixed.append(f"{self._identifier_uri}/{scope}")
return prefixed
# --- Dependency injection support ---
# These require fastmcp[azure] extra for azure-identity
def _require_azure_identity(feature: str) -> None:
"""Raise ImportError with install instructions if azure-identity is not available."""
try:
import azure.identity # noqa: F401
except ImportError as e:
raise ImportError(
f"{feature} requires the `azure` extra. "
"Install with: pip install 'fastmcp[azure]'"
) from e
def _find_azure_provider(auth: AuthProvider | None) -> AzureProvider | None:
"""Extract an AzureProvider from an auth provider, unwrapping MultiAuth if needed."""
if isinstance(auth, AzureProvider):
return auth
if isinstance(auth, MultiAuth) and isinstance(auth.server, AzureProvider):
return auth.server
return None
class _EntraOBOToken(Dependency[str]):
"""Dependency that performs OBO token exchange for Microsoft Entra.
Uses azure.identity's OnBehalfOfCredential for async-native OBO,
with automatic token caching and refresh. Credentials are cached on
the AzureProvider so repeated tool calls reuse existing credentials
and benefit from the Azure SDK's internal token cache.
"""
def __init__(self, scopes: list[str]):
self.scopes = scopes
async def __aenter__(self) -> str:
_require_azure_identity("EntraOBOToken")
from fastmcp.server.dependencies import get_access_token, get_server
access_token = get_access_token()
if access_token is None:
raise RuntimeError(
"No access token available. Cannot perform OBO exchange."
)
server = get_server()
azure_provider = _find_azure_provider(server.auth)
if azure_provider is None:
raise RuntimeError(
"EntraOBOToken requires an AzureProvider as the auth provider. "
f"Current provider: {type(server.auth).__name__}"
)
credential = await azure_provider.get_obo_credential(
user_assertion=access_token.token,
)
result = await credential.get_token(*self.scopes)
return result.token
def EntraOBOToken(scopes: list[str]) -> str:
"""Exchange the user's Entra token for a downstream API token via OBO.
This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
allowing your MCP server to call downstream APIs (like Microsoft Graph) on
behalf of the authenticated user.
Args:
scopes: The scopes to request for the downstream API. For Microsoft Graph,
use scopes like ["https://graph.microsoft.com/Mail.Read"] or
["https://graph.microsoft.com/.default"].
Returns:
A dependency that resolves to the downstream API access token string
Raises:
ImportError: If fastmcp[azure] is not installed
RuntimeError: If no access token is available, provider is not Azure,
or OBO exchange fails
Example:
```python
from fastmcp.server.plugins.auth.azure.provider import EntraOBOToken
import httpx
@mcp.tool()
async def get_my_emails(
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"])
):
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://graph.microsoft.com/v1.0/me/messages",
headers={"Authorization": f"Bearer {graph_token}"}
)
return resp.json()
```
Note:
For OBO to work, ensure the scopes are included in the AzureProvider's
`additional_authorize_scopes` parameter, and that admin consent has been
granted for those scopes in your Entra app registration.
"""
return cast(str, _EntraOBOToken(scopes))

View file

@ -0,0 +1,5 @@
"""Clerk auth plugin."""
from fastmcp.server.plugins.auth.clerk.plugin import ClerkAuth
__all__ = ["ClerkAuth"]

View file

@ -0,0 +1,67 @@
"""Clerk auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
from fastmcp.server.plugins.base import PluginMeta
class ClerkAuthConfig(OAuthProviderConfig):
"""Config model for the Clerk auth plugin."""
domain: str | None = None
valid_scopes: list[str] | None = None
extra_authorize_params: dict[str, str] | None = None
class ClerkAuth(AuthPlugin[ClerkAuthConfig]):
"""Contribute a `ClerkProvider` as the server's auth provider."""
Config: ClassVar[type[ClerkAuthConfig]] = ClerkAuthConfig
meta = PluginMeta(name="clerk-auth")
def __init__(
self,
config: ClerkAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
self._require("domain", "client_id", "base_url")
self._require_one("client_secret", "jwt_signing_key")
return ClerkProvider(
**self._kwargs(
"domain",
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"valid_scopes",
"timeout_seconds",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"extra_authorize_params",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)

View file

@ -0,0 +1,388 @@
"""Clerk OAuth provider for FastMCP.
This module provides a complete Clerk OAuth integration that's ready to use
with a Clerk domain, client ID, and client secret. It handles all the complexity
of Clerk's OAuth/OIDC flow, token validation, and user management.
Clerk uses standard OIDC endpoints derived from the instance domain
(e.g., ``https://<instance>.clerk.accounts.dev``). Token verification is
performed via the introspection endpoint (RFC 7662) for security-critical
checks (active status, audience, scopes), followed by the userinfo endpoint
for profile enrichment. Userinfo failure is non-fatal.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
auth = ClerkProvider(
domain="saving-primate-16.clerk.accounts.dev",
client_id="your-clerk-client-id",
client_secret="your-clerk-client-secret",
base_url="https://my-server.com",
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
from __future__ import annotations
import contextlib
from typing import Literal
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class ClerkTokenVerifier(TokenVerifier):
"""Token verifier for Clerk OAuth tokens.
Clerk issues standard OIDC tokens. Verification uses the introspection
endpoint (RFC 7662) as the primary security gate it confirms the token
is active and provides metadata (scopes, expiry, audience). The userinfo
endpoint is called second for profile enrichment (name, email, picture)
and its failure is non-fatal.
When a ``client_id`` is configured, the audience from introspection is
validated against it. When ``required_scopes`` are configured,
introspection must return the token's scopes — the verifier will not
assume scopes when introspection is unavailable.
"""
def __init__(
self,
*,
domain: str,
client_id: str | None = None,
client_secret: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Clerk token verifier.
Args:
domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev")
client_id: Clerk OAuth client ID, used for introspection endpoint authentication
client_secret: Clerk OAuth client secret, used for introspection endpoint authentication
required_scopes: Required OAuth scopes (e.g., ["openid", "email", "profile"])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.domain = domain.rstrip("/")
self._client_id = client_id
self._client_secret = client_secret
self.timeout_seconds = timeout_seconds
self._http_client = http_client
self._userinfo_url = f"https://{self.domain}/oauth/userinfo"
self._introspection_url = f"https://{self.domain}/oauth/token_info"
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify a Clerk OAuth token via introspection and userinfo.
Calls the introspection endpoint first to validate the token and
retrieve auth metadata (active status, scopes, expiry, audience).
If the token passes security checks, the userinfo endpoint is called
for profile enrichment. Userinfo failure is non-fatal.
When a ``client_id`` is configured, the token's audience must match it.
When ``required_scopes`` are configured, introspection must confirm
them; tokens are rejected if scope information is unavailable.
"""
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Step 1: Validate token via introspection (RFC 7662).
# Security-critical checks (active, audience, scopes) come first.
introspect_data_payload: dict = {"token": token}
introspect_kwargs: dict = {
"data": introspect_data_payload,
"headers": {"User-Agent": "FastMCP-Clerk-OAuth"},
}
if self._client_id and self._client_secret:
introspect_kwargs["auth"] = (
self._client_id,
self._client_secret,
)
elif self._client_id:
introspect_data_payload["client_id"] = self._client_id
introspect_response = await client.post(
self._introspection_url,
**introspect_kwargs,
)
if introspect_response.status_code != 200:
logger.debug(
"Clerk introspection failed: %d",
introspect_response.status_code,
)
return None
introspect_data = introspect_response.json()
# RFC 7662 requires the 'active' field in the response.
# A missing field indicates a malformed response — reject.
if "active" not in introspect_data or not introspect_data["active"]:
logger.debug(
"Clerk introspection: token inactive or missing 'active' field"
)
return None
scope_str = introspect_data.get("scope", "")
token_scopes = scope_str.split() if scope_str else []
aud = introspect_data.get("aud") or introspect_data.get("client_id")
expires_at: int | None = None
exp = introspect_data.get("exp")
if exp is not None:
with contextlib.suppress(ValueError, TypeError):
expires_at = int(exp)
if self._client_id and aud != self._client_id:
logger.debug(
"Clerk token audience mismatch: got %s, expected %s",
aud,
self._client_id,
)
return None
if self.required_scopes:
if not token_scopes:
logger.debug(
"Clerk token missing scope information; "
"cannot verify required scopes %s",
self.required_scopes,
)
return None
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"Clerk token missing required scopes. Has %s, needs %s",
token_scopes_set,
required_scopes_set,
)
return None
# Step 2: Fetch user profile via userinfo.
# Enriches the token with profile data (name, email, picture).
sub = introspect_data.get("sub")
user_data: dict = {}
try:
userinfo_response = await client.get(
self._userinfo_url,
headers={
"Authorization": f"Bearer {token}",
"User-Agent": "FastMCP-Clerk-OAuth",
},
)
if userinfo_response.status_code == 200:
user_data = userinfo_response.json()
if not sub:
sub = user_data.get("sub")
except Exception as e:
logger.debug("Clerk userinfo call failed: %s", e)
if not sub:
logger.debug("Clerk token missing 'sub' claim")
return None
access_token = AccessToken(
token=token,
client_id=aud or sub,
scopes=token_scopes,
expires_at=expires_at,
claims={
"sub": sub,
"aud": aud,
"email": user_data.get("email"),
"email_verified": user_data.get("email_verified"),
"name": user_data.get("name"),
"picture": user_data.get("picture"),
"given_name": user_data.get("given_name"),
"family_name": user_data.get("family_name"),
"preferred_username": user_data.get("preferred_username"),
"iss": user_data.get("iss"),
"clerk_user_data": user_data or None,
},
)
logger.debug("Clerk token verified successfully for sub=%s", sub)
return access_token
except httpx.RequestError as e:
logger.debug("Failed to verify Clerk token: %s", e)
return None
except Exception as e:
logger.debug("Clerk token verification error: %s", e)
return None
class ClerkProvider(OAuthProxy):
"""Complete Clerk OAuth provider for FastMCP.
This provider makes it trivial to add Clerk OAuth protection to any
FastMCP server. Provide your Clerk instance domain, OAuth app credentials,
and a base URL, and you're ready to go.
Clerk uses standard OIDC endpoints derived from the instance domain.
All endpoint URLs are constructed automatically from the domain parameter.
Features:
- Transparent OAuth proxy to Clerk
- Automatic token validation via Clerk's userinfo & introspection APIs
- User information extraction from Clerk's OIDC claims
- PKCE support (S256)
- Minimal configuration required
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
auth = ClerkProvider(
domain="saving-primate-16.clerk.accounts.dev",
client_id="your-clerk-client-id",
client_secret="your-clerk-client-secret",
base_url="https://my-server.com",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
domain: str,
client_id: str,
client_secret: str | None = None,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
valid_scopes: list[str] | None = None,
timeout_seconds: int = 10,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
extra_authorize_params: dict[str, str] | None = None,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize Clerk OAuth provider.
Args:
domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev").
This is used to derive all OAuth/OIDC endpoint URLs.
client_id: Clerk OAuth application client ID
client_secret: Clerk OAuth application client secret.
Optional for PKCE public clients. When omitted, jwt_signing_key must be provided.
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Clerk OAuth app (defaults to "/auth/callback")
required_scopes: Required Clerk scopes (defaults to ["openid", "email", "profile"]).
Clerk supports: "openid", "email", "profile", "public_metadata",
"private_metadata", "offline_access".
valid_scopes: All scopes that clients are allowed to request, advertised through
well-known endpoints. Defaults to required_scopes if not provided.
timeout_seconds: HTTP request timeout for Clerk API calls (defaults to 10)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from ``platformdirs``).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes
are provided, they will be used as is. If a string is provided, it will be derived
into a 32-byte key. If not provided, the upstream client secret will be used to
derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing
clients (default True). When "external", the built-in consent screen is skipped
but no warning is logged, indicating that consent is handled externally by Clerk.
consent_csp_policy: Custom CSP policy for the consent page.
extra_authorize_params: Additional parameters to forward to Clerk's authorization
endpoint. Example: {"prompt": "login"} to force re-authentication.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created
per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
domain = domain.rstrip("/")
required_scopes_final = (
parse_scopes(required_scopes)
if required_scopes is not None
else ["openid", "email", "profile"]
)
parsed_valid_scopes = (
parse_scopes(valid_scopes) if valid_scopes is not None else None
)
token_verifier = ClerkTokenVerifier(
domain=domain,
client_id=client_id,
client_secret=client_secret,
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
extra_authorize_params_final = (
dict(extra_authorize_params) if extra_authorize_params else {}
)
super().__init__(
upstream_authorization_endpoint=f"https://{domain}/oauth/authorize",
upstream_token_endpoint=f"https://{domain}/oauth/token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
extra_authorize_params=extra_authorize_params_final or None,
valid_scopes=parsed_valid_scopes,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized Clerk OAuth provider for domain %s with scopes: %s",
domain,
required_scopes_final,
)

View file

@ -0,0 +1,5 @@
"""Descope auth plugin."""
from fastmcp.server.plugins.auth.descope.plugin import DescopeAuth
__all__ = ["DescopeAuth"]

View file

@ -0,0 +1,55 @@
"""Descope auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider, TokenVerifier
from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
from fastmcp.server.plugins.base import PluginMeta
class DescopeAuthConfig(RemoteAuthConfig):
"""Config model for the Descope auth plugin."""
config_url: AnyHttpUrl | str | None = None
project_id: str | None = None
descope_base_url: AnyHttpUrl | str | None = None
class DescopeAuth(AuthPlugin[DescopeAuthConfig]):
"""Contribute a `DescopeProvider` as the server's auth provider."""
Config: ClassVar[type[DescopeAuthConfig]] = DescopeAuthConfig
meta = PluginMeta(name="descope-auth")
def __init__(
self,
config: DescopeAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
self._require("base_url")
if self.config.config_url is None:
self._require("project_id", "descope_base_url")
return DescopeProvider(
**self._kwargs(
"base_url",
"config_url",
"project_id",
"descope_base_url",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)

View file

@ -0,0 +1,209 @@
"""Descope authentication provider for FastMCP.
This module provides DescopeProvider - a complete authentication solution that integrates
with Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)
for seamless MCP client authentication.
"""
from __future__ import annotations
from urllib.parse import urlparse
import httpx
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class DescopeProvider(RemoteAuthProvider):
"""Descope metadata provider for DCR (Dynamic Client Registration).
This provider implements Descope integration using metadata forwarding.
This is the recommended approach for Descope DCR
as it allows Descope to handle the OAuth flow directly while FastMCP acts
as a resource server.
IMPORTANT SETUP REQUIREMENTS:
1. Create an MCP Server in Descope Console:
- Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console
- Create a new MCP Server
- Ensure that **Dynamic Client Registration (DCR)** is enabled
- Note your Well-Known URL
2. Note your Well-Known URL:
- Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)
- Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``
For detailed setup instructions, see:
https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
Example:
```python
from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
# Create Descope metadata provider (JWT verifier created automatically)
descope_auth = DescopeProvider(
config_url="https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration",
base_url="https://your-fastmcp-server.com",
)
# Use with FastMCP
mcp = FastMCP("My App", auth=descope_auth)
```
"""
def __init__(
self,
*,
base_url: AnyHttpUrl | str,
config_url: AnyHttpUrl | str | None = None,
project_id: str | None = None,
descope_base_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Descope metadata provider.
Args:
base_url: Public URL of this FastMCP server
config_url: Your Descope Well-Known URL (e.g., "https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration")
This is the new recommended way. If provided, project_id and descope_base_url are ignored.
project_id: Your Descope Project ID (e.g., "P2abc123"). Used with descope_base_url for backwards compatibility.
descope_base_url: Your Descope base URL (e.g., "https://api.descope.com"). Used with project_id for backwards compatibility.
required_scopes: Optional list of scopes that must be present in validated tokens.
These scopes will be included in the protected resource metadata.
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
"""
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
# Parse scopes if provided as string
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else None
)
# Determine which API is being used
if config_url is not None:
# New API: use config_url
# Strip /.well-known/openid-configuration from config_url if present
issuer_url = str(config_url)
if issuer_url.endswith("/.well-known/openid-configuration"):
issuer_url = issuer_url[: -len("/.well-known/openid-configuration")]
# Parse the issuer URL to extract descope_base_url and project_id for other uses
parsed_url = urlparse(issuer_url)
path_parts = parsed_url.path.strip("/").split("/")
# Extract project_id from path (format: /v1/apps/agentic/P.../M...)
if "agentic" in path_parts:
agentic_index = path_parts.index("agentic")
if agentic_index + 1 < len(path_parts):
self.project_id = path_parts[agentic_index + 1]
else:
raise ValueError(
f"Could not extract project_id from config_url: {issuer_url}"
)
else:
raise ValueError(
f"Could not find 'agentic' in config_url path: {issuer_url}"
)
# Extract descope_base_url (scheme + netloc)
self.descope_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}".rstrip(
"/"
)
elif project_id is not None and descope_base_url is not None:
# Old API: use project_id and descope_base_url
self.project_id = project_id
descope_base_url_str = str(descope_base_url).rstrip("/")
# Ensure descope_base_url has a scheme
if not descope_base_url_str.startswith(("http://", "https://")):
descope_base_url_str = f"https://{descope_base_url_str}"
self.descope_base_url = descope_base_url_str
# Old issuer format
issuer_url = f"{self.descope_base_url}/v1/apps/{self.project_id}"
else:
raise ValueError(
"Either config_url (new API) or both project_id and descope_base_url (old API) must be provided"
)
# Create default JWT verifier if none provided
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.descope_base_url}/{self.project_id}/.well-known/jwks.json",
issuer=issuer_url,
algorithm="RS256",
audience=self.project_id,
required_scopes=parsed_scopes,
)
# Initialize RemoteAuthProvider with Descope as the authorization server
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(issuer_url)],
base_url=self.base_url,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get OAuth routes including Descope authorization server metadata forwarding.
This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards Descope's OAuth metadata to clients.
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
# Get the standard protected resource routes from RemoteAuthProvider
routes = super().get_routes(mcp_path)
async def oauth_authorization_server_metadata(request):
"""Forward Descope OAuth authorization server metadata with FastMCP customizations."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.descope_base_url}/v1/apps/{self.project_id}/.well-known/oauth-authorization-server"
)
response.raise_for_status()
metadata = response.json()
return JSONResponse(metadata)
except Exception as e:
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch Descope metadata: {e}",
},
status_code=500,
)
# Add Descope authorization server metadata forwarding
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes

View file

@ -0,0 +1,5 @@
"""Discord auth plugin."""
from fastmcp.server.plugins.auth.discord.plugin import DiscordAuth
__all__ = ["DiscordAuth"]

View file

@ -0,0 +1,59 @@
"""Discord auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
from fastmcp.server.plugins.base import PluginMeta
class DiscordAuthConfig(OAuthProviderConfig):
"""Config model for the Discord auth plugin."""
class DiscordAuth(AuthPlugin[DiscordAuthConfig]):
"""Contribute a `DiscordProvider` as the server's auth provider."""
Config: ClassVar[type[DiscordAuthConfig]] = DiscordAuthConfig
meta = PluginMeta(name="discord-auth")
def __init__(
self,
config: DiscordAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
self._require("client_id", "client_secret", "base_url")
return DiscordProvider(
**self._kwargs(
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"timeout_seconds",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)

View file

@ -0,0 +1,288 @@
"""Discord OAuth provider for FastMCP.
This module provides a complete Discord OAuth integration that's ready to use
with just a client ID and client secret. It handles all the complexity of
Discord's OAuth flow, token validation, and user management.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
# Simple Discord OAuth protection
auth = DiscordProvider(
client_id="your-discord-client-id",
client_secret="your-discord-client-secret"
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
from __future__ import annotations
import contextlib
import time
from datetime import datetime
from typing import Literal
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class DiscordTokenVerifier(TokenVerifier):
"""Token verifier for Discord OAuth tokens.
Discord OAuth tokens are opaque (not JWTs), so we verify them
by calling Discord's tokeninfo API to check if they're valid and get user info.
"""
def __init__(
self,
*,
expected_client_id: str,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Discord token verifier.
Args:
expected_client_id: Expected Discord OAuth client ID for audience binding
required_scopes: Required OAuth scopes (e.g., ['email'])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.expected_client_id = expected_client_id
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify Discord OAuth token by calling Discord's tokeninfo API."""
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Use Discord's tokeninfo endpoint to validate the token
headers = {
"Authorization": f"Bearer {token}",
"User-Agent": "FastMCP-Discord-OAuth",
}
response = await client.get(
"https://discord.com/api/oauth2/@me",
headers=headers,
)
if response.status_code != 200:
logger.debug(
"Discord token verification failed: %d",
response.status_code,
)
return None
token_info = response.json()
# Check if token is expired (Discord returns ISO timestamp)
expires_str = token_info.get("expires")
expires_at = None
if expires_str:
expires_dt = datetime.fromisoformat(
expires_str.replace("Z", "+00:00")
)
expires_at = int(expires_dt.timestamp())
if expires_at <= int(time.time()):
logger.debug("Discord token has expired")
return None
token_scopes = token_info.get("scopes", [])
# Check required scopes
if self.required_scopes:
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"Discord token missing required scopes. Has %d, needs %d",
len(token_scopes_set),
len(required_scopes_set),
)
return None
user_data = token_info.get("user", {})
application = token_info.get("application") or {}
client_id = str(application.get("id", "unknown"))
if client_id != self.expected_client_id:
logger.debug(
"Discord token app ID mismatch: expected %s, got %s",
self.expected_client_id,
client_id,
)
return None
# Create AccessToken with Discord user info
access_token = AccessToken(
token=token,
client_id=client_id,
scopes=token_scopes,
expires_at=expires_at,
claims={
"sub": user_data.get("id"),
"username": user_data.get("username"),
"discriminator": user_data.get("discriminator"),
"avatar": user_data.get("avatar"),
"email": user_data.get("email"),
"verified": user_data.get("verified"),
"locale": user_data.get("locale"),
"discord_user": user_data,
"discord_token_info": token_info,
},
)
logger.debug("Discord token verified successfully")
return access_token
except httpx.RequestError as e:
logger.debug("Failed to verify Discord token: %s", e)
return None
except Exception as e:
logger.debug("Discord token verification error: %s", e)
return None
class DiscordProvider(OAuthProxy):
"""Complete Discord OAuth provider for FastMCP.
This provider makes it trivial to add Discord OAuth protection to any
FastMCP server. Just provide your Discord OAuth app credentials and
a base URL, and you're ready to go.
Features:
- Transparent OAuth proxy to Discord
- Automatic token validation via Discord's API
- User information extraction from Discord APIs
- Minimal configuration required
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
auth = DiscordProvider(
client_id="123456789",
client_secret="discord-client-secret-abc123...",
base_url="https://my-server.com"
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize Discord OAuth provider.
Args:
client_id: Discord OAuth client ID (e.g., "123456789")
client_secret: Discord OAuth client secret (e.g., "S....")
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Discord OAuth app (defaults to "/auth/callback")
required_scopes: Required Discord scopes (defaults to ["identify"]). Common scopes include:
- "identify" for profile info (default)
- "email" for email access
- "guilds" for server membership info
timeout_seconds: HTTP request timeout for Discord API calls (defaults to 10)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Discord.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes)
if required_scopes is not None
else ["identify"]
)
# Create Discord token verifier
token_verifier = DiscordTokenVerifier(
expected_client_id=client_id,
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Initialize OAuth proxy with Discord endpoints
super().__init__(
upstream_authorization_endpoint="https://discord.com/oauth2/authorize",
upstream_token_endpoint="https://discord.com/api/oauth2/token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized Discord OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)

View file

@ -0,0 +1,5 @@
"""GitHub auth plugin."""
from fastmcp.server.plugins.auth.github.plugin import GitHubAuth
__all__ = ["GitHubAuth"]

View file

@ -0,0 +1,64 @@
"""GitHub auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from fastmcp.server.plugins.base import PluginMeta
class GitHubAuthConfig(OAuthProviderConfig):
"""Config model for the GitHub auth plugin."""
cache_ttl_seconds: int | None = None
max_cache_size: int | None = None
class GitHubAuth(AuthPlugin[GitHubAuthConfig]):
"""Contribute a `GitHubProvider` as the server's auth provider."""
Config: ClassVar[type[GitHubAuthConfig]] = GitHubAuthConfig
meta = PluginMeta(name="github-auth")
def __init__(
self,
config: GitHubAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
self._require("client_id", "client_secret", "base_url")
return GitHubProvider(
**self._kwargs(
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"timeout_seconds",
"cache_ttl_seconds",
"max_cache_size",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)

View file

@ -0,0 +1,303 @@
"""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.plugins.auth.github.provider 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)
```
"""
from __future__ import annotations
import contextlib
from typing import Literal
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.token_cache import TokenCache
logger = get_logger(__name__)
class GitHubTokenVerifier(TokenVerifier):
"""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.
Caching is disabled by default. Set ``cache_ttl_seconds`` to a positive
integer to cache successful verification results and avoid repeated
GitHub API calls for the same token.
"""
def __init__(
self,
*,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
cache_ttl_seconds: int | None = None,
max_cache_size: int | None = None,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the GitHub token verifier.
Args:
required_scopes: Required OAuth scopes (e.g., ['user:email'])
timeout_seconds: HTTP request timeout
cache_ttl_seconds: How long to cache verification results in seconds.
Caching is disabled by default (None). Set to a positive integer
to enable (e.g., 300 for 5 minutes).
max_cache_size: Maximum number of tokens to cache. Default: 10 000.
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.timeout_seconds = timeout_seconds
self._http_client = http_client
self._cache = TokenCache(
ttl_seconds=cache_ttl_seconds,
max_size=max_cache_size,
)
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify GitHub OAuth token by calling GitHub API."""
is_cached, cached_result = self._cache.get(token)
if is_cached:
logger.debug("GitHub token cache hit")
return cached_result
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Get token info from GitHub API
response = await client.get(
"https://api.github.com/user",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "FastMCP-GitHub-OAuth",
},
)
if response.status_code != 200:
logger.debug(
"GitHub token verification failed: %d - %s",
response.status_code,
response.text[:200],
)
return None
user_data = response.json()
# Get token scopes from GitHub API
# GitHub includes scopes in the X-OAuth-Scopes header
scopes_response = await client.get(
"https://api.github.com/user/repos", # Any authenticated endpoint
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "FastMCP-GitHub-OAuth",
},
)
# Extract scopes from X-OAuth-Scopes header if available
scopes_verified = scopes_response.status_code == 200
oauth_scopes_header = scopes_response.headers.get("x-oauth-scopes", "")
token_scopes = [
scope.strip()
for scope in oauth_scopes_header.split(",")
if scope.strip()
]
# If no scopes in header, assume basic scopes based on successful user API call
if not token_scopes:
token_scopes = ["user"] # Basic scope if we can access user info
# Check required scopes
if self.required_scopes:
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"GitHub token missing required scopes. Has %d, needs %d",
len(token_scopes_set),
len(required_scopes_set),
)
return None
# Create AccessToken with GitHub user info
result = AccessToken(
token=token,
client_id=str(user_data.get("id", "unknown")), # Use GitHub user ID
scopes=token_scopes,
expires_at=None, # GitHub tokens don't typically expire
claims={
"sub": str(user_data["id"]),
"login": user_data.get("login"),
"name": user_data.get("name"),
"email": user_data.get("email"),
"avatar_url": user_data.get("avatar_url"),
"github_user_data": user_data,
},
)
if scopes_verified:
self._cache.set(token, result)
return result
except httpx.RequestError as e:
logger.debug("Failed to verify GitHub token: %s", e)
return None
except Exception as e:
logger.debug("GitHub token verification error: %s", e)
return None
class GitHubProvider(OAuthProxy):
"""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
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="Ov23li...",
client_secret="abc123...",
base_url="https://my-server.com"
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
cache_ttl_seconds: int | None = None,
max_cache_size: int | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize GitHub OAuth provider.
Args:
client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
client_secret: GitHub OAuth app client secret
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
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 (defaults to 10)
cache_ttl_seconds: How long to cache token verification results in seconds.
Caching is disabled by default (None). Set to a positive integer to
enable (e.g., 300 for 5 minutes).
max_cache_size: Maximum number of tokens to cache. Default: 10 000.
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to GitHub.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
SECURITY WARNING: Only set to False for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["user"]
)
# Create GitHub token verifier
token_verifier = GitHubTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
cache_ttl_seconds=cache_ttl_seconds,
max_cache_size=max_cache_size,
http_client=http_client,
)
# Initialize OAuth proxy with GitHub endpoints
super().__init__(
upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
upstream_token_endpoint="https://github.com/login/oauth/access_token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized GitHub OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)

View file

@ -0,0 +1,5 @@
"""Google auth plugin."""
from fastmcp.server.plugins.auth.google.plugin import GoogleAuth
__all__ = ["GoogleAuth"]

View file

@ -0,0 +1,65 @@
"""Google auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
from fastmcp.server.plugins.auth.google.provider import GoogleProvider
from fastmcp.server.plugins.base import PluginMeta
class GoogleAuthConfig(OAuthProviderConfig):
"""Config model for the Google auth plugin."""
valid_scopes: list[str] | None = None
extra_authorize_params: dict[str, str] | None = None
class GoogleAuth(AuthPlugin[GoogleAuthConfig]):
"""Contribute a `GoogleProvider` as the server's auth provider."""
Config: ClassVar[type[GoogleAuthConfig]] = GoogleAuthConfig
meta = PluginMeta(name="google-auth")
def __init__(
self,
config: GoogleAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
self._require("client_id", "base_url")
self._require_one("client_secret", "jwt_signing_key")
return GoogleProvider(
**self._kwargs(
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"valid_scopes",
"timeout_seconds",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"extra_authorize_params",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)

View file

@ -0,0 +1,365 @@
"""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.plugins.auth.google.provider 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)
```
"""
from __future__ import annotations
import contextlib
import time
from typing import Literal
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
GOOGLE_SCOPE_ALIASES: dict[str, str] = {
"email": "https://www.googleapis.com/auth/userinfo.email",
"profile": "https://www.googleapis.com/auth/userinfo.profile",
}
def _normalize_google_scope(scope: str) -> str:
"""Normalize a Google scope shorthand to its canonical full URI.
Google accepts shorthand scopes like "email" and "profile" in authorization
requests, but returns the full URI form in token responses. This normalizes
to the full URI so comparisons work regardless of which form was used.
"""
return GOOGLE_SCOPE_ALIASES.get(scope, scope)
class GoogleTokenVerifier(TokenVerifier):
"""Token verifier for Google OAuth tokens.
Google OAuth tokens are opaque (not JWTs), so we verify them by calling
Google's tokeninfo endpoint with the access token as a query parameter.
This returns the OAuth app ID (``aud``), granted scopes, and expiry time.
User profile data (name, picture, etc.) is fetched separately from the
v2 userinfo endpoint when the token is valid.
"""
def __init__(
self,
*,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Google token verifier.
Args:
required_scopes: Required OAuth scopes (e.g., ['openid', 'https://www.googleapis.com/auth/userinfo.email'])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
normalized = (
[_normalize_google_scope(s) for s in required_scopes]
if required_scopes
else required_scopes
)
super().__init__(required_scopes=normalized)
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify a Google OAuth token using the tokeninfo endpoint.
Calls ``https://oauth2.googleapis.com/tokeninfo?access_token=TOKEN``
to validate the token and retrieve the OAuth app ID (``aud``), granted
scopes, and expiry time. On success, fetches user profile data from
the v2 userinfo endpoint to populate name, picture, and locale claims.
"""
try:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Step 1: Verify token via tokeninfo endpoint.
# Returns aud (OAuth app ID), scope (space-separated), expires_in, sub, email.
response = await client.get(
"https://oauth2.googleapis.com/tokeninfo",
params={"access_token": token},
headers={"User-Agent": "FastMCP-Google-OAuth"},
)
if response.status_code != 200:
logger.debug(
"Google token verification failed: %d",
response.status_code,
)
return None
token_data = response.json()
# aud is the OAuth app ID (client_id / audience)
aud = token_data.get("aud")
if not aud:
logger.debug("Google tokeninfo missing 'aud' claim")
return None
# sub is required (unique Google user ID)
sub = token_data.get("sub")
if not sub:
logger.debug("Google tokeninfo missing 'sub' claim")
return None
# Parse scopes directly from the tokeninfo response (space-separated)
scope_str = token_data.get("scope", "")
token_scopes = scope_str.split() if scope_str else []
# Check required scopes
if self.required_scopes:
token_scopes_set = set(token_scopes)
required_scopes_set = set(self.required_scopes)
if not required_scopes_set.issubset(token_scopes_set):
logger.debug(
"Google token missing required scopes. Has %d, needs %d",
len(token_scopes_set),
len(required_scopes_set),
)
return None
# Compute expiry from expires_in (seconds until expiry)
expires_at: int | None = None
expires_in = token_data.get("expires_in")
if expires_in is not None:
with contextlib.suppress(ValueError, TypeError):
expires_at = int(time.time()) + int(expires_in)
# Step 2: Fetch user profile from v2 userinfo endpoint.
# tokeninfo provides auth data; userinfo provides name, picture, locale.
user_data: dict = {}
try:
userinfo_response = await client.get(
"https://www.googleapis.com/oauth2/v2/userinfo",
headers={
"Authorization": f"Bearer {token}",
"User-Agent": "FastMCP-Google-OAuth",
},
)
if userinfo_response.status_code == 200:
user_data = userinfo_response.json()
except Exception as e:
logger.debug("Failed to fetch Google user profile: %s", e)
access_token = AccessToken(
token=token,
client_id=sub,
scopes=token_scopes,
expires_at=expires_at,
claims={
"sub": sub,
"aud": aud,
"email": token_data.get("email") or user_data.get("email"),
"email_verified": token_data.get("email_verified")
or user_data.get("verified_email"),
"name": user_data.get("name"),
"picture": user_data.get("picture"),
"given_name": user_data.get("given_name"),
"family_name": user_data.get("family_name"),
"locale": user_data.get("locale"),
"google_user_data": user_data or None,
},
)
logger.debug("Google token verified successfully")
return access_token
except httpx.RequestError as e:
logger.debug("Failed to verify Google token: %s", e)
return None
except Exception as e:
logger.debug("Google token verification error: %s", e)
return None
class GoogleProvider(OAuthProxy):
"""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
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.google.provider import GoogleProvider
auth = GoogleProvider(
client_id="123456789.apps.googleusercontent.com",
client_secret="GOCSPX-abc123...",
base_url="https://my-server.com"
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
client_id: str,
client_secret: str | None = None,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
required_scopes: list[str] | None = None,
valid_scopes: list[str] | None = None,
timeout_seconds: int = 10,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
extra_authorize_params: dict[str, str] | None = None,
http_client: httpx.AsyncClient | None = None,
enable_cimd: bool = True,
):
"""Initialize Google OAuth provider.
Args:
client_id: Google OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
client_secret: Google OAuth client secret (e.g., "GOCSPX-abc123...").
Optional for PKCE public clients (e.g., native apps). When omitted,
jwt_signing_key must be provided.
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
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
Google scope shorthands like "email" and "profile" are automatically
normalized to their full URI forms for token verification.
valid_scopes: All scopes that clients are allowed to request, advertised through
well-known endpoints. Defaults to required_scopes if not provided. Use this
when you want clients to be able to request additional scopes beyond the
required minimum. Shorthands are normalized to full URI forms.
timeout_seconds: HTTP request timeout for Google API calls (defaults to 10)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
If None, an encrypted file store will be created in the data directory
(derived from `platformdirs`).
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
When True, users see a consent screen before being redirected to Google.
When False, authorization proceeds directly without user confirmation.
When "external", the built-in consent screen is skipped but no warning is
logged, indicating that consent is handled externally (e.g. by Google's own consent).
SECURITY WARNING: Only set to False for local development or testing environments.
extra_authorize_params: Additional parameters to forward to Google's authorization endpoint.
By default, GoogleProvider sets {"access_type": "offline", "prompt": "consent"} to ensure
refresh tokens are returned. You can override these defaults or add additional parameters.
Example: {"prompt": "select_account"} to let users choose their Google account.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
client IDs (default True). Set to False to disable.
"""
# Parse scopes if provided as string
# Google requires at least one scope - openid is the minimal OIDC scope
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
# Normalize valid_scopes if provided
parsed_valid_scopes = (
parse_scopes(valid_scopes) if valid_scopes is not None else None
)
valid_scopes_final = (
[_normalize_google_scope(s) for s in parsed_valid_scopes]
if parsed_valid_scopes is not None
else None
)
# Create Google token verifier
# Normalization of shorthand scopes (e.g. "email" -> full URI) happens
# inside GoogleTokenVerifier so required_scopes match what Google returns.
token_verifier = GoogleTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Set Google-specific defaults for extra authorize params
# access_type=offline ensures refresh tokens are returned
# prompt=consent forces consent screen to get refresh token (Google only issues on first auth otherwise)
google_defaults = {
"access_type": "offline",
"prompt": "consent",
}
# User-provided params override defaults
if extra_authorize_params:
google_defaults.update(extra_authorize_params)
extra_authorize_params_final = google_defaults
# Initialize OAuth proxy with Google endpoints
super().__init__(
upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
upstream_token_endpoint="https://oauth2.googleapis.com/token",
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=base_url,
resource_base_url=resource_base_url,
redirect_path=redirect_path,
issuer_url=issuer_url or base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
extra_authorize_params=extra_authorize_params_final,
valid_scopes=valid_scopes_final,
enable_cimd=enable_cimd,
)
logger.debug(
"Initialized Google OAuth provider for client %s with scopes: %s",
client_id,
required_scopes_final,
)

View file

@ -0,0 +1,5 @@
"""Keycloak auth plugin."""
from fastmcp.server.plugins.auth.keycloak.plugin import KeycloakAuth
__all__ = ["KeycloakAuth"]

View file

@ -0,0 +1,45 @@
"""Keycloak auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider, TokenVerifier
from fastmcp.server.plugins.auth._base import AuthPlugin, PluginConfig
from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
from fastmcp.server.plugins.base import PluginMeta
class KeycloakAuthConfig(PluginConfig):
"""Config model for the Keycloak auth plugin."""
realm_url: AnyHttpUrl | str | None = None
base_url: AnyHttpUrl | str | None = None
required_scopes: list[str] | str | None = None
audience: str | list[str] | None = None
class KeycloakAuth(AuthPlugin[KeycloakAuthConfig]):
"""Contribute a `KeycloakAuthProvider` as the server's auth provider."""
Config: ClassVar[type[KeycloakAuthConfig]] = KeycloakAuthConfig
meta = PluginMeta(name="keycloak-auth")
def __init__(
self,
config: KeycloakAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
self._require("realm_url", "base_url")
return KeycloakAuthProvider(
**self._kwargs("realm_url", "base_url", "required_scopes", "audience"),
token_verifier=self._token_verifier,
)

View file

@ -0,0 +1,74 @@
"""Keycloak authentication provider for FastMCP."""
from __future__ import annotations
from pydantic import AnyHttpUrl
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class KeycloakAuthProvider(RemoteAuthProvider):
"""Keycloak authentication provider using Dynamic Client Registration (DCR).
Requires Keycloak 26.6.0 or later, which includes the fix for DCR compatibility
with MCP clients (https://github.com/keycloak/keycloak/pull/45309).
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
auth = KeycloakAuthProvider(
realm_url="https://keycloak.example.com/realms/myrealm",
base_url="https://my-mcp-server.example.com",
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
realm_url: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
required_scopes: list[str] | str | None = None,
audience: str | list[str] | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize the Keycloak auth provider.
Args:
realm_url: Keycloak realm URL (e.g., "https://keycloak.example.com/realms/myrealm")
base_url: Public URL of this FastMCP server
required_scopes: Scopes to require on incoming tokens. Defaults to
["openid"], which ensures the `sub` claim (user identifier) is
present in the access token. Override to require additional scopes.
audience: Optional audience(s) for JWT validation. Recommended for production.
token_verifier: Optional custom token verifier. Defaults to a JWTVerifier
configured for Keycloak's JWKS endpoint and issuer.
"""
self.realm_url = str(realm_url).rstrip("/")
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.realm_url}/protocol/openid-connect/certs",
issuer=self.realm_url,
algorithm="RS256",
required_scopes=parsed_scopes,
audience=audience,
)
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(self.realm_url)],
base_url=AnyHttpUrl(str(base_url).rstrip("/")),
)

View file

@ -0,0 +1,5 @@
"""OCI auth plugin."""
from fastmcp.server.plugins.auth.oci.plugin import OCIAuth
__all__ = ["OCIAuth"]

View file

@ -0,0 +1,61 @@
"""OCI auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProxyConfig
from fastmcp.server.plugins.auth.oci.provider import OCIProvider
from fastmcp.server.plugins.base import PluginMeta
class OCIAuthConfig(OAuthProxyConfig):
"""Config model for the OCI auth plugin."""
config_url: AnyHttpUrl | str | None = None
client_id: str | None = None
client_secret: str | None = None
audience: str | None = None
class OCIAuth(AuthPlugin[OCIAuthConfig]):
"""Contribute an `OCIProvider` as the server's auth provider."""
Config: ClassVar[type[OCIAuthConfig]] = OCIAuthConfig
meta = PluginMeta(name="oci-auth")
def __init__(
self,
config: OCIAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
def auth(self) -> AuthProvider | None:
self._require("config_url", "client_id", "client_secret", "base_url")
return OCIProvider(
**self._kwargs(
"config_url",
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"audience",
"issuer_url",
"required_scopes",
"redirect_path",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
),
client_storage=self._client_storage,
)

View file

@ -0,0 +1,180 @@
"""OCI OIDC provider for FastMCP.
The pull request for the provider is submitted to fastmcp.
This module provides OIDC Implementation to integrate MCP servers with OCI.
You only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.
Post Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.
You need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.
The sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.
You can use the signer object to create OCI service object.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.oci.provider import OCIProvider
from fastmcp.server.dependencies import get_access_token
from fastmcp.utilities.logging import get_logger
import os
import oci
from oci.auth.signers import TokenExchangeSigner
logger = get_logger(__name__)
# Load configuration from environment
config_url = os.environ.get("OCI_CONFIG_URL") # OCI IAM Domain OIDC discovery URL
client_id = os.environ.get("OCI_CLIENT_ID") # Client ID configured for the OCI IAM Domain Integrated Application
client_secret = os.environ.get("OCI_CLIENT_SECRET") # Client secret configured for the OCI IAM Domain Integrated Application
iam_guid = os.environ.get("OCI_IAM_GUID") # IAM GUID configured for the OCI IAM Domain
# Simple OCI OIDC protection
auth = OCIProvider(
config_url=config_url, # config URL is the OCI IAM Domain OIDC discovery URL
client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
required_scopes=["openid", "profile", "email"],
redirect_path="/auth/callback",
base_url="http://localhost:8000",
)
# NOTE: For production use, replace this with a thread-safe cache implementation
# such as threading.Lock-protected dict or a proper caching library
_global_token_cache = {} # In memory cache for OCI session token signer
def get_oci_signer() -> TokenExchangeSigner:
authntoken = get_access_token()
tokenID = authntoken.claims.get("jti")
token = authntoken.token
# Check if the signer exists for the token ID in memory cache
cached_signer = _global_token_cache.get(tokenID)
logger.debug(f"Global cached signer: {cached_signer}")
if cached_signer:
logger.debug(f"Using globally cached signer for token ID: {tokenID}")
return cached_signer
# If the signer is not yet created for the token then create new OCI signer object
logger.debug(f"Creating new signer for token ID: {tokenID}")
signer = TokenExchangeSigner(
jwt_or_func=token,
oci_domain_id=iam_guid.split(".")[0] if iam_guid else None, # This is same as IAM GUID configured for the OCI IAM Domain
client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
)
logger.debug(f"Signer {signer} created for token ID: {tokenID}")
#Cache the signer object in memory cache
_global_token_cache[tokenID] = signer
logger.debug(f"Signer cached for token ID: {tokenID}")
return signer
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
from typing import Literal
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class OCIProvider(OIDCProxy):
"""An OCI IAM Domain provider implementation for FastMCP.
This provider is a complete OCI integration that's ready to use with
just the configuration URL, client ID, client secret, and base URL.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.oci.provider import OCIProvider
import os
# Load configuration from environment
auth = OCIProvider(
config_url=os.environ.get("OCI_CONFIG_URL"), # OCI IAM Domain OIDC discovery URL
client_id=os.environ.get("OCI_CLIENT_ID"), # Client ID configured for the OCI IAM Domain Integrated Application
client_secret=os.environ.get("OCI_CLIENT_SECRET"), # Client secret configured for the OCI IAM Domain Integrated Application
base_url="http://localhost:8000",
required_scopes=["openid", "profile", "email"],
redirect_path="/auth/callback",
)
mcp = FastMCP("My Protected Server", auth=auth)
```
"""
def __init__(
self,
*,
config_url: AnyHttpUrl | str,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
audience: str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | None = None,
redirect_path: str | None = None,
allowed_client_redirect_uris: list[str] | None = None,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
) -> None:
"""Initialize OCI OIDC provider.
Args:
config_url: OCI OIDC Discovery URL
client_id: OCI IAM Domain Integrated Application client id
client_secret: OCI Integrated Application client secret
base_url: Public URL where OIDC endpoints will be accessible (includes any mount path)
resource_base_url: Optional public base URL for the protected resource metadata
and token audience. Defaults to ``base_url``.
audience: OCI API audience (optional)
issuer_url: Issuer URL for OCI IAM Domain metadata. This will override issuer URL from the discovery URL.
required_scopes: Required OCI scopes (defaults to ["openid"])
redirect_path: Redirect path configured in OCI IAM Domain Integrated Application. The default is "/auth/callback".
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
"""
# Parse scopes if provided as string
oci_required_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
super().__init__(
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
audience=audience,
base_url=base_url,
resource_base_url=resource_base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
required_scopes=oci_required_scopes,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
)
logger.debug(
"Initialized OCI OAuth provider for client %s with scopes: %s",
client_id,
oci_required_scopes,
)

View file

@ -0,0 +1,5 @@
"""PropelAuth auth plugin."""
from fastmcp.server.plugins.auth.propelauth.plugin import PropelAuth
__all__ = ["PropelAuth"]

View file

@ -0,0 +1,77 @@
"""PropelAuth auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
import httpx
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider
from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
from fastmcp.server.plugins.auth.propelauth.provider import (
PropelAuthProvider,
PropelAuthTokenIntrospectionOverrides,
)
from fastmcp.server.plugins.base import PluginMeta
class PropelAuthConfig(RemoteAuthConfig):
"""Config model for the PropelAuth auth plugin."""
auth_url: AnyHttpUrl | str | None = None
introspection_client_id: str | None = None
introspection_client_secret: str | None = None
resource: AnyHttpUrl | str | None = None
introspection_timeout_seconds: int | None = None
introspection_cache_ttl_seconds: int | None = None
introspection_max_cache_size: int | None = None
class PropelAuth(AuthPlugin[PropelAuthConfig]):
"""Contribute a `PropelAuthProvider` as the server's auth provider."""
Config: ClassVar[type[PropelAuthConfig]] = PropelAuthConfig
meta = PluginMeta(name="propelauth-auth")
def __init__(
self,
config: PropelAuthConfig | dict[str, Any] | None = None,
*,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._http_client = http_client
def auth(self) -> AuthProvider | None:
self._require(
"auth_url",
"introspection_client_id",
"introspection_client_secret",
"base_url",
)
overrides: PropelAuthTokenIntrospectionOverrides = {}
if self.config.introspection_timeout_seconds is not None:
overrides["timeout_seconds"] = self.config.introspection_timeout_seconds
if self.config.introspection_cache_ttl_seconds is not None:
overrides["cache_ttl_seconds"] = self.config.introspection_cache_ttl_seconds
if self.config.introspection_max_cache_size is not None:
overrides["max_cache_size"] = self.config.introspection_max_cache_size
if self._http_client is not None:
overrides["http_client"] = self._http_client
return PropelAuthProvider(
**self._kwargs(
"auth_url",
"introspection_client_id",
"introspection_client_secret",
"base_url",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
"resource",
),
token_introspection_overrides=overrides or None,
)

View file

@ -0,0 +1,234 @@
"""PropelAuth authentication provider for FastMCP.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.propelauth.provider import PropelAuthProvider
auth = PropelAuthProvider(
auth_url="https://auth.yourdomain.com",
introspection_client_id="your-client-id",
introspection_client_secret="your-client-secret",
base_url="https://your-fastmcp-server.com",
required_scopes=["read:user_data"],
)
mcp = FastMCP("My App", auth=auth)
```
"""
from __future__ import annotations
from typing import TypedDict
import httpx
from pydantic import AnyHttpUrl, SecretStr
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp.server.auth import AccessToken, RemoteAuthProvider
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class PropelAuthTokenIntrospectionOverrides(TypedDict, total=False):
timeout_seconds: int
cache_ttl_seconds: int | None
max_cache_size: int | None
http_client: httpx.AsyncClient | None
class PropelAuthProvider(RemoteAuthProvider):
"""PropelAuth resource server provider using OAuth 2.1 token introspection.
This provider validates access tokens via PropelAuth's introspection endpoint
and forwards authorization server metadata for OAuth discovery.
Setup:
1. Enable MCP authentication in the PropelAuth Dashboard
2. Configure scopes on the MCP page
3. Select which redirect URIs to enable by picking which clients you support
4. Generate introspection credentials (Client ID + Client Secret)
For detailed setup instructions, see:
https://docs.propelauth.com/mcp-authentication/overview
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.plugins.auth.propelauth.provider import PropelAuthProvider
auth = PropelAuthProvider(
auth_url="https://auth.yourdomain.com",
introspection_client_id="your-client-id",
introspection_client_secret="your-client-secret",
base_url="https://your-fastmcp-server.com",
required_scopes=["read:user_data"],
)
mcp = FastMCP("My App", auth=auth)
```
"""
def __init__(
self,
*,
auth_url: AnyHttpUrl | str,
introspection_client_id: str,
introspection_client_secret: str | SecretStr,
base_url: AnyHttpUrl | str,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
resource: AnyHttpUrl | str | None = None,
token_introspection_overrides: (
PropelAuthTokenIntrospectionOverrides | None
) = None,
):
"""Initialize PropelAuth provider.
Args:
auth_url: Your PropelAuth Auth URL (from the Backend Integration page)
introspection_client_id: Introspection Client ID from the PropelAuth Dashboard
introspection_client_secret: Introspection Client Secret from the PropelAuth Dashboard
base_url: Public URL of this FastMCP server
required_scopes: Optional list of scopes that must be present in tokens
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
resource: Optional resource URI (RFC 8707) identifying this MCP server.
Use this when multiple MCP servers share the same PropelAuth
authorization server (e.g. ``resource="https://api.example.com/mcp"``),
so only tokens intended for this MCP server are accepted.
token_introspection_overrides: Optional overrides for the underlying
IntrospectionTokenVerifier (timeout, caching, http_client)
"""
normalized_auth_url = str(auth_url).rstrip("/")
introspection_url = f"{normalized_auth_url}/oauth/2.1/introspect"
authorization_server_url = AnyHttpUrl(f"{normalized_auth_url}/oauth/2.1")
if resource is None:
self._resource = None
logger.debug(
"PropelAuthProvider: no resource configured, audience checking disabled"
)
else:
self._resource = str(resource)
token_verifier = self._create_token_verifier(
introspection_url=introspection_url,
client_id=introspection_client_id,
client_secret=introspection_client_secret,
required_scopes=required_scopes,
introspection_overrides=token_introspection_overrides,
)
self._normalized_auth_url = normalized_auth_url
super().__init__(
token_verifier=token_verifier,
authorization_servers=[authorization_server_url],
base_url=base_url,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get routes for this provider.
Includes the standard routes from the RemoteAuthProvider (protected resource metadata routes (RFC 9728)),
and creates an authorization server metadata route that forwards to PropelAuth's route
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
routes = super().get_routes(mcp_path)
async def oauth_authorization_server_metadata(request):
"""Forward PropelAuth OAuth authorization server metadata"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self._normalized_auth_url}/.well-known/oauth-authorization-server/oauth/2.1"
)
response.raise_for_status()
metadata = response.json()
return JSONResponse(metadata)
except Exception as e:
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch PropelAuth metadata: {e}",
},
status_code=500,
)
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify token and check the ``aud`` claim against the configured resource."""
result = await super().verify_token(token)
if result is None or self._resource is None:
return result
aud = result.claims.get("aud")
if aud != self._resource:
logger.debug(
"PropelAuthProvider: token audience %r does not match resource %s",
aud,
self._resource,
)
return None
return result
def _create_token_verifier(
self,
introspection_url: str,
client_id: str,
client_secret: str | SecretStr,
required_scopes: list[str] | None,
introspection_overrides: PropelAuthTokenIntrospectionOverrides | None,
) -> IntrospectionTokenVerifier:
# Being defensive here, check for only the fields we are expecting
safe_overrides: PropelAuthTokenIntrospectionOverrides = {}
if introspection_overrides is not None:
if "timeout_seconds" in introspection_overrides:
safe_overrides["timeout_seconds"] = introspection_overrides[
"timeout_seconds"
]
if "cache_ttl_seconds" in introspection_overrides:
safe_overrides["cache_ttl_seconds"] = introspection_overrides[
"cache_ttl_seconds"
]
if "max_cache_size" in introspection_overrides:
safe_overrides["max_cache_size"] = introspection_overrides[
"max_cache_size"
]
if "http_client" in introspection_overrides:
safe_overrides["http_client"] = introspection_overrides["http_client"]
return IntrospectionTokenVerifier(
introspection_url=introspection_url,
client_id=client_id,
client_secret=client_secret,
required_scopes=required_scopes,
**safe_overrides,
)

View file

@ -1,792 +0,0 @@
"""First-party auth plugins.
These plugins are thin, JSON-configurable wrappers around FastMCP's
existing auth providers. Python-only dependencies such as HTTP clients,
token verifiers, and client storage stay as constructor arguments.
"""
from __future__ import annotations
from typing import Any, Generic, Literal, TypeVar
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, BaseModel, ConfigDict
from fastmcp.server.auth import AuthProvider, TokenVerifier
from fastmcp.server.plugins.base import Plugin, PluginMeta
ConsentMode = bool | Literal["remember", "external"]
Algorithm = Literal["RS256", "ES256"]
ConfigT = TypeVar("ConfigT", bound=BaseModel)
class _AuthPlugin(Plugin[ConfigT], Generic[ConfigT]):
def _require(self, *fields: str) -> None:
missing = [field for field in fields if getattr(self.config, field) is None]
if missing:
names = ", ".join(f"`{field}`" for field in missing)
raise ValueError(f"{type(self).__name__} requires {names}.")
def _require_one(self, *fields: str) -> None:
if not any(getattr(self.config, field) is not None for field in fields):
names = " or ".join(f"`{field}`" for field in fields)
raise ValueError(f"{type(self).__name__} requires {names}.")
def _kwargs(self, *fields: str) -> dict[str, Any]:
return {
field: getattr(self.config, field)
for field in fields
if getattr(self.config, field) is not None
}
class _PluginConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
class _OAuthProxyConfig(_PluginConfig):
base_url: AnyHttpUrl | str | None = None
resource_base_url: AnyHttpUrl | str | None = None
issuer_url: AnyHttpUrl | str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
require_authorization_consent: ConsentMode = True
consent_csp_policy: str | None = None
forward_resource: bool = True
class _OAuthProviderConfig(_OAuthProxyConfig):
client_id: str | None = None
client_secret: str | None = None
timeout_seconds: int = 10
enable_cimd: bool = True
class _RemoteAuthConfig(_PluginConfig):
base_url: AnyHttpUrl | str | None = None
required_scopes: list[str] | None = None
scopes_supported: list[str] | None = None
resource_name: str | None = None
resource_documentation: AnyHttpUrl | None = None
class Auth0AuthConfig(_OAuthProxyConfig):
"""Config model for the Auth0 auth plugin."""
config_url: AnyHttpUrl | str | None = None
client_id: str | None = None
client_secret: str | None = None
audience: str | None = None
class Auth0Auth(_AuthPlugin[Auth0AuthConfig]):
"""Contribute an `Auth0Provider` as the server's auth provider."""
meta = PluginMeta(name="auth0-auth")
def __init__(
self,
config: Auth0AuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.auth0 import Auth0Provider
self._require(
"config_url", "client_id", "client_secret", "audience", "base_url"
)
return Auth0Provider(
**self._kwargs(
"config_url",
"client_id",
"client_secret",
"audience",
"base_url",
"resource_base_url",
"issuer_url",
"required_scopes",
"redirect_path",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
),
client_storage=self._client_storage,
)
class AuthKitAuthConfig(_RemoteAuthConfig):
"""Config model for the WorkOS AuthKit auth plugin."""
authkit_domain: AnyHttpUrl | str | None = None
resource_base_url: AnyHttpUrl | str | None = None
class AuthKitAuth(_AuthPlugin[AuthKitAuthConfig]):
"""Contribute an `AuthKitProvider` as the server's auth provider."""
meta = PluginMeta(name="authkit-auth")
def __init__(
self,
config: AuthKitAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.workos import AuthKitProvider
self._require("authkit_domain", "base_url")
return AuthKitProvider(
**self._kwargs(
"authkit_domain",
"base_url",
"resource_base_url",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)
class AWSCognitoAuthConfig(_OAuthProxyConfig):
"""Config model for the AWS Cognito auth plugin."""
user_pool_id: str | None = None
client_id: str | None = None
client_secret: str | None = None
aws_region: str = "eu-central-1"
redirect_path: str | None = "/auth/callback"
class AWSCognitoAuth(_AuthPlugin[AWSCognitoAuthConfig]):
"""Contribute an `AWSCognitoProvider` as the server's auth provider."""
meta = PluginMeta(name="aws-cognito-auth")
def __init__(
self,
config: AWSCognitoAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.aws import AWSCognitoProvider
self._require("user_pool_id", "client_id", "client_secret", "base_url")
return AWSCognitoProvider(
**self._kwargs(
"user_pool_id",
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"aws_region",
"issuer_url",
"redirect_path",
"required_scopes",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
),
client_storage=self._client_storage,
)
class AzureAuthConfig(_OAuthProviderConfig):
"""Config model for the Azure auth plugin."""
tenant_id: str | None = None
required_scopes: list[str] | None = None
identifier_uri: str | None = None
additional_authorize_scopes: list[str] | None = None
base_authority: str = "login.microsoftonline.com"
class AzureAuth(_AuthPlugin[AzureAuthConfig]):
"""Contribute an `AzureProvider` as the server's auth provider."""
meta = PluginMeta(name="azure-auth")
def __init__(
self,
config: AzureAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.azure import AzureProvider
self._require("client_id", "tenant_id", "required_scopes", "base_url")
self._require_one("client_secret", "jwt_signing_key")
return AzureProvider(
**self._kwargs(
"client_id",
"client_secret",
"tenant_id",
"required_scopes",
"base_url",
"resource_base_url",
"identifier_uri",
"issuer_url",
"redirect_path",
"additional_authorize_scopes",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"base_authority",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)
class ClerkAuthConfig(_OAuthProviderConfig):
"""Config model for the Clerk auth plugin."""
domain: str | None = None
valid_scopes: list[str] | None = None
extra_authorize_params: dict[str, str] | None = None
class ClerkAuth(_AuthPlugin[ClerkAuthConfig]):
"""Contribute a `ClerkProvider` as the server's auth provider."""
meta = PluginMeta(name="clerk-auth")
def __init__(
self,
config: ClerkAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.clerk import ClerkProvider
self._require("domain", "client_id", "base_url")
self._require_one("client_secret", "jwt_signing_key")
return ClerkProvider(
**self._kwargs(
"domain",
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"valid_scopes",
"timeout_seconds",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"extra_authorize_params",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)
class DescopeAuthConfig(_RemoteAuthConfig):
"""Config model for the Descope auth plugin."""
config_url: AnyHttpUrl | str | None = None
project_id: str | None = None
descope_base_url: AnyHttpUrl | str | None = None
class DescopeAuth(_AuthPlugin[DescopeAuthConfig]):
"""Contribute a `DescopeProvider` as the server's auth provider."""
meta = PluginMeta(name="descope-auth")
def __init__(
self,
config: DescopeAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.descope import DescopeProvider
self._require("base_url")
if self.config.config_url is None:
self._require("project_id", "descope_base_url")
return DescopeProvider(
**self._kwargs(
"base_url",
"config_url",
"project_id",
"descope_base_url",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)
class DiscordAuthConfig(_OAuthProviderConfig):
"""Config model for the Discord auth plugin."""
class DiscordAuth(_AuthPlugin[DiscordAuthConfig]):
"""Contribute a `DiscordProvider` as the server's auth provider."""
meta = PluginMeta(name="discord-auth")
def __init__(
self,
config: DiscordAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.discord import DiscordProvider
self._require("client_id", "client_secret", "base_url")
return DiscordProvider(
**self._kwargs(
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"timeout_seconds",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)
class GitHubAuthConfig(_OAuthProviderConfig):
"""Config model for the GitHub auth plugin."""
cache_ttl_seconds: int | None = None
max_cache_size: int | None = None
class GitHubAuth(_AuthPlugin[GitHubAuthConfig]):
"""Contribute a `GitHubProvider` as the server's auth provider."""
meta = PluginMeta(name="github-auth")
def __init__(
self,
config: GitHubAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.github import GitHubProvider
self._require("client_id", "client_secret", "base_url")
return GitHubProvider(
**self._kwargs(
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"timeout_seconds",
"cache_ttl_seconds",
"max_cache_size",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)
class GoogleAuthConfig(_OAuthProviderConfig):
"""Config model for the Google auth plugin."""
valid_scopes: list[str] | None = None
extra_authorize_params: dict[str, str] | None = None
class GoogleAuth(_AuthPlugin[GoogleAuthConfig]):
"""Contribute a `GoogleProvider` as the server's auth provider."""
meta = PluginMeta(name="google-auth")
def __init__(
self,
config: GoogleAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.google import GoogleProvider
self._require("client_id", "base_url")
self._require_one("client_secret", "jwt_signing_key")
return GoogleProvider(
**self._kwargs(
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"valid_scopes",
"timeout_seconds",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"extra_authorize_params",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)
class KeycloakAuthConfig(_PluginConfig):
"""Config model for the Keycloak auth plugin."""
realm_url: AnyHttpUrl | str | None = None
base_url: AnyHttpUrl | str | None = None
required_scopes: list[str] | str | None = None
audience: str | list[str] | None = None
class KeycloakAuth(_AuthPlugin[KeycloakAuthConfig]):
"""Contribute a `KeycloakAuthProvider` as the server's auth provider."""
meta = PluginMeta(name="keycloak-auth")
def __init__(
self,
config: KeycloakAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
self._require("realm_url", "base_url")
return KeycloakAuthProvider(
**self._kwargs("realm_url", "base_url", "required_scopes", "audience"),
token_verifier=self._token_verifier,
)
class OCIAuthConfig(_OAuthProxyConfig):
"""Config model for the OCI auth plugin."""
config_url: AnyHttpUrl | str | None = None
client_id: str | None = None
client_secret: str | None = None
audience: str | None = None
class OCIAuth(_AuthPlugin[OCIAuthConfig]):
"""Contribute an `OCIProvider` as the server's auth provider."""
meta = PluginMeta(name="oci-auth")
def __init__(
self,
config: OCIAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.oci import OCIProvider
self._require("config_url", "client_id", "client_secret", "base_url")
return OCIProvider(
**self._kwargs(
"config_url",
"client_id",
"client_secret",
"base_url",
"resource_base_url",
"audience",
"issuer_url",
"required_scopes",
"redirect_path",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
),
client_storage=self._client_storage,
)
class PropelAuthConfig(_RemoteAuthConfig):
"""Config model for the PropelAuth auth plugin."""
auth_url: AnyHttpUrl | str | None = None
introspection_client_id: str | None = None
introspection_client_secret: str | None = None
resource: AnyHttpUrl | str | None = None
introspection_timeout_seconds: int | None = None
introspection_cache_ttl_seconds: int | None = None
introspection_max_cache_size: int | None = None
class PropelAuth(_AuthPlugin[PropelAuthConfig]):
"""Contribute a `PropelAuthProvider` as the server's auth provider."""
meta = PluginMeta(name="propelauth-auth")
def __init__(
self,
config: PropelAuthConfig | dict[str, Any] | None = None,
*,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._http_client = http_client
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.propelauth import (
PropelAuthProvider,
PropelAuthTokenIntrospectionOverrides,
)
self._require(
"auth_url",
"introspection_client_id",
"introspection_client_secret",
"base_url",
)
overrides: PropelAuthTokenIntrospectionOverrides = {}
if self.config.introspection_timeout_seconds is not None:
overrides["timeout_seconds"] = self.config.introspection_timeout_seconds
if self.config.introspection_cache_ttl_seconds is not None:
overrides["cache_ttl_seconds"] = self.config.introspection_cache_ttl_seconds
if self.config.introspection_max_cache_size is not None:
overrides["max_cache_size"] = self.config.introspection_max_cache_size
if self._http_client is not None:
overrides["http_client"] = self._http_client
return PropelAuthProvider(
**self._kwargs(
"auth_url",
"introspection_client_id",
"introspection_client_secret",
"base_url",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
"resource",
),
token_introspection_overrides=overrides or None,
)
class ScalekitAuthConfig(_RemoteAuthConfig):
"""Config model for the Scalekit auth plugin."""
environment_url: AnyHttpUrl | str | None = None
resource_id: str | None = None
mcp_url: AnyHttpUrl | str | None = None
client_id: str | None = None
class ScalekitAuth(_AuthPlugin[ScalekitAuthConfig]):
"""Contribute a `ScalekitProvider` as the server's auth provider."""
meta = PluginMeta(name="scalekit-auth")
def __init__(
self,
config: ScalekitAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
self._require("environment_url", "resource_id")
self._require_one("base_url", "mcp_url")
return ScalekitProvider(
**self._kwargs(
"environment_url",
"resource_id",
"base_url",
"mcp_url",
"client_id",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)
class SupabaseAuthConfig(_RemoteAuthConfig):
"""Config model for the Supabase auth plugin."""
project_url: AnyHttpUrl | str | None = None
auth_route: str = "/auth/v1"
algorithm: Algorithm = "ES256"
class SupabaseAuth(_AuthPlugin[SupabaseAuthConfig]):
"""Contribute a `SupabaseProvider` as the server's auth provider."""
meta = PluginMeta(name="supabase-auth")
def __init__(
self,
config: SupabaseAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.supabase import SupabaseProvider
self._require("project_url", "base_url")
return SupabaseProvider(
**self._kwargs(
"project_url",
"base_url",
"auth_route",
"algorithm",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)
class WorkOSAuthConfig(_OAuthProviderConfig):
"""Config model for the WorkOS auth plugin."""
authkit_domain: str | None = None
class WorkOSAuth(_AuthPlugin[WorkOSAuthConfig]):
"""Contribute a `WorkOSProvider` as the server's auth provider."""
meta = PluginMeta(name="workos-auth")
def __init__(
self,
config: WorkOSAuthConfig | dict[str, Any] | None = None,
*,
client_storage: AsyncKeyValue | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
super().__init__(config)
self._client_storage = client_storage
self._http_client = http_client
def auth(self) -> AuthProvider | None:
from fastmcp.server.auth.providers.workos import WorkOSProvider
self._require("client_id", "client_secret", "authkit_domain", "base_url")
return WorkOSProvider(
**self._kwargs(
"client_id",
"client_secret",
"authkit_domain",
"base_url",
"resource_base_url",
"issuer_url",
"redirect_path",
"required_scopes",
"timeout_seconds",
"allowed_client_redirect_uris",
"jwt_signing_key",
"require_authorization_consent",
"consent_csp_policy",
"forward_resource",
"enable_cimd",
),
client_storage=self._client_storage,
http_client=self._http_client,
)

View file

@ -0,0 +1,5 @@
"""Scalekit auth plugin."""
from fastmcp.server.plugins.auth.scalekit.plugin import ScalekitAuth
__all__ = ["ScalekitAuth"]

View file

@ -0,0 +1,56 @@
"""Scalekit auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider, TokenVerifier
from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
from fastmcp.server.plugins.base import PluginMeta
class ScalekitAuthConfig(RemoteAuthConfig):
"""Config model for the Scalekit auth plugin."""
environment_url: AnyHttpUrl | str | None = None
resource_id: str | None = None
mcp_url: AnyHttpUrl | str | None = None
client_id: str | None = None
class ScalekitAuth(AuthPlugin[ScalekitAuthConfig]):
"""Contribute a `ScalekitProvider` as the server's auth provider."""
Config: ClassVar[type[ScalekitAuthConfig]] = ScalekitAuthConfig
meta = PluginMeta(name="scalekit-auth")
def __init__(
self,
config: ScalekitAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
self._require("environment_url", "resource_id")
self._require_one("base_url", "mcp_url")
return ScalekitProvider(
**self._kwargs(
"environment_url",
"resource_id",
"base_url",
"mcp_url",
"client_id",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)

View file

@ -0,0 +1,212 @@
"""Scalekit authentication provider for FastMCP.
This module provides ScalekitProvider - a complete authentication solution that integrates
with Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server
authentication for seamless MCP client authentication.
"""
from __future__ import annotations
import httpx
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class ScalekitProvider(RemoteAuthProvider):
"""Scalekit resource server provider for OAuth 2.1 authentication.
This provider implements Scalekit integration using resource server pattern.
FastMCP acts as a protected resource server that validates access tokens issued
by Scalekit's authorization server.
IMPORTANT SETUP REQUIREMENTS:
1. Create an MCP Server in Scalekit Dashboard:
- Go to your [Scalekit Dashboard](https://app.scalekit.com/)
- Navigate to MCP Servers section
- Register a new MCP Server with appropriate scopes
- Ensure the Resource Identifier matches exactly what you configure as MCP URL
- Note the Resource ID
2. Environment Configuration:
- Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
- Set SCALEKIT_RESOURCE_ID from your created resource
- Set BASE_URL to your FastMCP server's public URL
For detailed setup instructions, see:
https://docs.scalekit.com/mcp/overview/
Example:
```python
from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
# Create Scalekit resource server provider
scalekit_auth = ScalekitProvider(
environment_url="https://your-env.scalekit.com",
resource_id="sk_resource_...",
base_url="https://your-fastmcp-server.com",
)
# Use with FastMCP
mcp = FastMCP("My App", auth=scalekit_auth)
```
"""
def __init__(
self,
*,
environment_url: AnyHttpUrl | str,
resource_id: str,
base_url: AnyHttpUrl | str | None = None,
mcp_url: AnyHttpUrl | str | None = None,
client_id: str | None = None,
required_scopes: list[str] | None = None,
scopes_supported: list[str] | None = None,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Scalekit resource server provider.
Args:
environment_url: Your Scalekit environment URL (e.g., "https://your-env.scalekit.com")
resource_id: Your Scalekit resource ID
base_url: Public URL of this FastMCP server (or use mcp_url for backwards compatibility)
mcp_url: Deprecated alias for base_url. Will be removed in a future release.
client_id: Deprecated parameter, no longer required. Will be removed in a future release.
required_scopes: Optional list of scopes that must be present in tokens
scopes_supported: Optional list of scopes to advertise in OAuth metadata.
If None, uses required_scopes. Use this when the scopes clients should
request differ from the scopes enforced on tokens.
resource_name: Optional name for the protected resource metadata.
resource_documentation: Optional documentation URL for the protected resource.
token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit
"""
# Resolve base_url from mcp_url if needed (backwards compatibility)
resolved_base_url = base_url or mcp_url
if not resolved_base_url:
raise ValueError("Either base_url or mcp_url must be provided")
if mcp_url is not None:
logger.warning(
"ScalekitProvider parameter 'mcp_url' is deprecated and will be removed in a future release. "
"Rename it to 'base_url'."
)
if client_id is not None:
logger.warning(
"ScalekitProvider no longer requires 'client_id'. The parameter is accepted only for backward "
"compatibility and will be removed in a future release."
)
self.environment_url = str(environment_url).rstrip("/")
self.resource_id = resource_id
parsed_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else []
)
self.required_scopes = parsed_scopes
base_url_value = str(resolved_base_url)
logger.debug(
"Initializing ScalekitProvider: environment_url=%s resource_id=%s base_url=%s required_scopes=%s",
self.environment_url,
self.resource_id,
base_url_value,
self.required_scopes,
)
# Create default JWT verifier if none provided
if token_verifier is None:
logger.debug(
"Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s",
f"{self.environment_url}/keys",
self.environment_url,
self.required_scopes,
)
token_verifier = JWTVerifier(
jwks_uri=f"{self.environment_url}/keys",
issuer=self.environment_url,
algorithm="RS256",
audience=self.resource_id,
required_scopes=self.required_scopes or None,
)
else:
logger.debug("Using custom token verifier for ScalekitProvider")
# Initialize RemoteAuthProvider with Scalekit as the authorization server
super().__init__(
token_verifier=token_verifier,
authorization_servers=[
AnyHttpUrl(f"{self.environment_url}/resources/{self.resource_id}")
],
base_url=base_url_value,
scopes_supported=scopes_supported,
resource_name=resource_name,
resource_documentation=resource_documentation,
)
def get_routes(
self,
mcp_path: str | None = None,
) -> list[Route]:
"""Get OAuth routes including Scalekit authorization server metadata forwarding.
This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards Scalekit's OAuth metadata to clients.
Args:
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
"""
# Get the standard protected resource routes from RemoteAuthProvider
routes = super().get_routes(mcp_path)
logger.debug(
"Preparing Scalekit metadata routes: mcp_path=%s resource_id=%s",
mcp_path,
self.resource_id,
)
async def oauth_authorization_server_metadata(request):
"""Forward Scalekit OAuth authorization server metadata with FastMCP customizations."""
try:
metadata_url = f"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}"
logger.debug(
"Fetching Scalekit OAuth metadata: metadata_url=%s", metadata_url
)
async with httpx.AsyncClient() as client:
response = await client.get(metadata_url)
response.raise_for_status()
metadata = response.json()
logger.debug(
"Scalekit metadata fetched successfully: metadata_keys=%s",
list(metadata.keys()),
)
return JSONResponse(metadata)
except Exception as e:
logger.error(f"Failed to fetch Scalekit metadata: {e}")
return JSONResponse(
{
"error": "server_error",
"error_description": f"Failed to fetch Scalekit metadata: {e}",
},
status_code=500,
)
# Add Scalekit authorization server metadata forwarding
routes.append(
Route(
"/.well-known/oauth-authorization-server",
endpoint=oauth_authorization_server_metadata,
methods=["GET"],
)
)
return routes

View file

@ -1,5 +0,0 @@
"""Supabase auth plugin."""
from fastmcp.server.plugins.auth.providers import SupabaseAuth, SupabaseAuthConfig
__all__ = ["SupabaseAuth", "SupabaseAuthConfig"]

View file

@ -0,0 +1,5 @@
"""Supabase auth plugin."""
from fastmcp.server.plugins.auth.supabase.plugin import SupabaseAuth
__all__ = ["SupabaseAuth"]

View file

@ -0,0 +1,53 @@
"""Supabase auth plugin."""
from __future__ import annotations
from typing import Any, ClassVar
from pydantic import AnyHttpUrl
from fastmcp.server.auth import AuthProvider, TokenVerifier
from fastmcp.server.plugins.auth._base import Algorithm, AuthPlugin, RemoteAuthConfig
from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
from fastmcp.server.plugins.base import PluginMeta
class SupabaseAuthConfig(RemoteAuthConfig):
"""Config model for the Supabase auth plugin."""
project_url: AnyHttpUrl | str | None = None
auth_route: str = "/auth/v1"
algorithm: Algorithm = "ES256"
class SupabaseAuth(AuthPlugin[SupabaseAuthConfig]):
"""Contribute a `SupabaseProvider` as the server's auth provider."""
Config: ClassVar[type[SupabaseAuthConfig]] = SupabaseAuthConfig
meta = PluginMeta(name="supabase-auth")
def __init__(
self,
config: SupabaseAuthConfig | dict[str, Any] | None = None,
*,
token_verifier: TokenVerifier | None = None,
) -> None:
super().__init__(config)
self._token_verifier = token_verifier
def auth(self) -> AuthProvider | None:
self._require("project_url", "base_url")
return SupabaseProvider(
**self._kwargs(
"project_url",
"base_url",
"auth_route",
"algorithm",
"required_scopes",
"scopes_supported",
"resource_name",
"resource_documentation",
),
token_verifier=self._token_verifier,
)

Some files were not shown because too many files have changed in this diff Show more