Remove automatic environment variable loading from auth providers (#2752)

This commit is contained in:
Jeremiah Lowin 2025-12-26 15:37:59 -05:00 committed by GitHub
commit 568300e8b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 640 additions and 3257 deletions

View file

@ -150,6 +150,31 @@ def my_prompt() -> Message:
See [Prompts documentation](/servers/prompts#return-values) for full details.
### Auth Provider Automatic Environment Variable Loading Removed
Auth providers no longer automatically read configuration from environment variables. You can still use environment variables, but you must read them yourself:
<CodeGroup>
```python Before
# Relied on FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc.
auth = GitHubProvider()
```
```python After
import os
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
base_url=os.environ["GITHUB_BASE_URL"],
)
```
</CodeGroup>
This applies to all auth providers: `GitHubProvider`, `GoogleProvider`, `AzureProvider`, `Auth0Provider`, `AWSProvider`, `WorkOSProvider`, `DescopeProvider`, `DiscordProvider`, `ScalekitProvider`, `SupabaseProvider`, `OCIProvider`, `JWTVerifier`, and `IntrospectionVerifier`.
The `FastMCPSettings` class has also been simplified - it no longer includes auth-related settings that were previously loaded from environment variables.
## v2.14.0
### OpenAPI Parser Promotion

View file

@ -0,0 +1,73 @@
---
title: Auth Provider Environment Variables
---
## Decision: Remove automatic environment variable loading from auth providers
You can still use environment variables for configuration - you just read them yourself with `os.environ` instead of relying on FastMCP's automatic loading.
**Status:** Implemented in v3.0.0
### Background
Auth providers in v2.x used `pydantic-settings` to automatically load configuration from environment variables with a `FASTMCP_SERVER_AUTH_<PROVIDER>_` prefix. For example, `GitHubProvider` would read from:
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID`
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET`
- `FASTMCP_SERVER_AUTH_GITHUB_BASE_URL`
- etc.
This was implemented via a `*ProviderSettings(BaseSettings)` class in each provider, combined with a `NotSet` sentinel pattern to distinguish between "not provided" and `None`.
### Why remove it
1. **Maintenance burden**: Every new provider needed to implement the settings class, validators, and the `NotSet` merging logic. This was ~50-100 lines of boilerplate per provider.
2. **Documentation complexity**: Each provider needed documentation explaining both the parameter and the corresponding environment variable. This doubled the surface area to document and maintain.
3. **Contributor friction**: New contributors adding providers had to understand and replicate this pattern, which was a source of inconsistency and bugs.
4. **Marginal user value**: Python developers are comfortable with `os.environ["VAR"]` or `os.environ.get("VAR", default)`. The automatic loading saved a single line of code per parameter while adding significant complexity.
5. **Implicit behavior**: Magic environment variable loading makes it harder to understand where values come from. Explicit `os.environ` calls are more traceable.
### Migration path
The migration is trivial - users add explicit environment variable reads:
```python
# Before (v2.x)
auth = GitHubProvider() # Relied on env vars
# After (v3.0)
import os
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
base_url=os.environ["MY_BASE_URL"],
)
```
Users can also use `os.environ.get()` with defaults, or any other configuration library they prefer (dotenv, dynaconf, etc.).
### Backwards compatibility
We chose not to provide backwards compatibility because:
1. This is a major version bump (v3.0), which is the appropriate time for breaking changes
2. The migration is straightforward (add `os.environ` calls)
3. Maintaining compatibility would require keeping all the boilerplate we're trying to remove
4. The pattern was likely not heavily used - most production deployments pass secrets explicitly rather than relying on magic prefixes
### What was removed
- `*ProviderSettings(BaseSettings)` classes from all auth providers
- `NotSet` sentinel usage in provider constructors
- `pydantic-settings` dependency for auth providers
- Environment variable documentation from provider docs
- Related test cases for env var loading
### Result
Provider constructors are now simple and explicit. Required parameters are actually required (Python raises `TypeError` if missing), and optional parameters have clear defaults. The code is more readable and easier to maintain.

View file

@ -194,84 +194,3 @@ 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>
## Environment Variables
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the Auth0 provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.auth0.Auth0Provider` to use Auth0 authentication.
</ParamField>
</Card>
### Auth0-Specific Configuration
These environment variables provide default values for the Auth0 provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL" required>
Your Auth0 Application Configuration URL (e.g., `https://.../.well-known/openid-configuration`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID" required>
Your Auth0 Application Client ID (e.g., `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET" required>
Your Auth0 Application Client Secret (e.g., `vPYqbjemq...`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE" required>
Your Auth0 API Audience
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_BASE_URL" required>
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_ISSUER_URL" default="Uses BASE_URL">
Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_REDIRECT_PATH" default="/auth/callback">
Redirect path configured in your Auth0 Application
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTH0_REQUIRED_SCOPES" default='["openid"]'>
Comma-, space-, or JSON-separated list of required AUth0 scopes (e.g., `openid email` or `["openid","email"]`)
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the Auth0 provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider
# Auth0 configuration and credentials
FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration
FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID=tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB
FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET=vPYqbjemq...
FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE=https://...
FASTMCP_SERVER_AUTH_AUTH0_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_AUTH0_REQUIRED_SCOPES=openid,email
```
With environment variables set, your server code simplifies to:
```python server.py
from fastmcp import FastMCP
# Authentication is automatically configured from environment
mcp = FastMCP(name="Auth0 Secured App")
@mcp.tool
async def search_logs() -> list[str]:
"""Search the service logs."""
# Your tool implementation here
pass
```

View file

@ -78,56 +78,20 @@ if __name__ == "__main__":
```
## Environment Variables
## Production Configuration
<VersionBadge version="2.12.1" />
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the AuthKit provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.workos.AuthKitProvider` to use AuthKit authentication.
</ParamField>
</Card>
### AuthKit-Specific Configuration
These environment variables provide default values for the AuthKit provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN" required>
Your AuthKit domain (e.g., `https://your-project-12345.authkit.app`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_BASE_URL" required>
Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000` for development)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_REQUIRED_SCOPES" default="[]">
Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid profile email` or `["openid", "profile", "email"]`)
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the AuthKit provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.AuthKitProvider
# AuthKit configuration
FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN=https://your-project-12345.authkit.app
FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_REQUIRED_SCOPES=openid,profile,email
```
With environment variables set, your server code simplifies to:
For production deployments, load sensitive configuration from environment variables:
```python server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
# Authentication is automatically configured from environment
mcp = FastMCP(name="AuthKit Secured App")
# 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")
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth)
```

View file

@ -238,92 +238,6 @@ Parameters (`jwt_signing_key` and `client_storage`) work together to ensure toke
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
</Note>
## Environment Variables
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the AWS Cognito provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.aws.AWSCognitoProvider` to use AWS Cognito authentication.
</ParamField>
</Card>
### AWS Cognito-Specific Configuration
These environment variables provide default values for the AWS Cognito provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID" required>
Your AWS Cognito user pool ID (e.g., `eu-central-1_XXXXXXXXX`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION" default="eu-central-1">
AWS region where your AWS Cognito user pool is located
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID" required>
Your AWS Cognito app client ID
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET" required>
Your AWS Cognito app client secret
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL" default="http://localhost:8000">
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_ISSUER_URL" default="Uses BASE_URL">
Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_REDIRECT_PATH" default="/auth/callback">
One of the redirect paths configured in your AWS Cognito app client
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AWS_COGNITO_REQUIRED_SCOPES" default='["openid"]'>
Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid email` or `["openid","email","profile"]`)
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the AWS Cognito provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.aws.AWSCognitoProvider
# AWS Cognito credentials
FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID=eu-central-1_XXXXXXXXX
FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION=eu-central-1
FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID=your-app-client-id
FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET=your-app-client-secret
FASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_AWS_COGNITO_REQUIRED_SCOPES=openid,email,profile
```
With environment variables set, your server code simplifies to:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_access_token
# Authentication is automatically configured from environment
mcp = FastMCP(name="AWS Cognito Secured App")
@mcp.tool
async def get_access_token_claims() -> dict:
"""Get the authenticated user's access token claims."""
token = get_access_token()
return {
"sub": token.claims.get("sub"),
"username": token.claims.get("username"),
"cognito:groups": token.claims.get("cognito:groups", []),
}
```
## Features
### JWT Token Validation

View file

@ -279,113 +279,3 @@ Parameters (`jwt_signing_key` and `client_storage`) work together to ensure toke
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
</Note>
## Environment Variables
<VersionBadge version="2.12.1" />
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the Azure provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.azure.AzureProvider` to use Azure authentication.
</ParamField>
</Card>
### Azure-Specific Configuration
These environment variables provide default values for the Azure provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID" required>
Your Azure App registration Client ID (e.g., `835f09b6-0f0f-40cc-85cb-f32c5829a149`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET" required>
Your Azure App registration Client Secret
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_TENANT_ID" required>
Your Azure tenant ID (specific ID, "organizations", or "consumers")
<Note>
This is **REQUIRED**. Find your tenant ID in Azure Portal under Microsoft Entra ID → Overview.
</Note>
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_BASE_URL" default="http://localhost:8000">
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_ISSUER_URL" default="Uses BASE_URL">
Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_REDIRECT_PATH" default="/auth/callback">
Redirect path configured in your Azure App registration
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES" required>
Comma-, space-, or JSON-separated list of required scopes for your API (at least one scope required). These are validated on tokens and used as defaults if the client does not request specific scopes. Use unprefixed scope names from your Azure App registration (e.g., `read,write`).
You can include standard OIDC scopes (`openid`, `profile`, `email`, `offline_access`) in `required_scopes`. FastMCP automatically handles them correctly: they're sent to Azure unprefixed and excluded from token validation (since Azure doesn't include OIDC scopes in access token `scp` claims).
<Note>
Azure's OAuth API requires the `scope` parameter - you must provide at least one scope.
</Note>
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_ADDITIONAL_AUTHORIZE_SCOPES" default="">
Comma-, space-, or JSON-separated list of additional scopes to include in the authorization request without prefixing. Use this to request upstream scopes such as Microsoft Graph permissions. These are not used for token validation.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_IDENTIFIER_URI" default="api://{client_id}">
Application ID URI used to prefix scopes during authorization.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_BASE_AUTHORITY" default="login.microsoftonline.com">
Azure authority base URL. Override this to use Azure Government:
- `login.microsoftonline.com` - Azure Public Cloud (default)
- `login.microsoftonline.us` - Azure Government
This setting affects all Azure OAuth endpoints (authorization, token, issuer, JWKS).
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the Azure provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.azure.AzureProvider
# Azure OAuth credentials
FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID=835f09b6-0f0f-40cc-85cb-f32c5829a149
FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET=your-client-secret-here
FASTMCP_SERVER_AUTH_AZURE_TENANT_ID=08541b6e-646d-43de-a0eb-834e6713d6d5
FASTMCP_SERVER_AUTH_AZURE_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES=read,write
# Optional custom API configuration
# FASTMCP_SERVER_AUTH_AZURE_IDENTIFIER_URI=api://your-api-id
# Request additional upstream scopes (optional)
# FASTMCP_SERVER_AUTH_AZURE_ADDITIONAL_AUTHORIZE_SCOPES=User.Read,Mail.Read
```
With environment variables set, your server code simplifies to:
```python server.py
from fastmcp import FastMCP
# Authentication is automatically configured from environment
mcp = FastMCP(name="Azure Secured App")
@mcp.tool
async def protected_tool(query: str) -> str:
"""A tool that requires Azure authentication to access."""
# Your tool implementation here
return f"Processing authenticated request: {query}"
```

View file

@ -95,52 +95,20 @@ if __name__ == "__main__":
asyncio.run(main())
```
## Environment Variables
## Production Configuration
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the Descope provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.descope.DescopeProvider` to use
Descope authentication.
</ParamField>
</Card>
### Descope-Specific Configuration
These environment variables provide default values for the Descope provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL" required>
Your Well-Known URL from the [Descope Console](https://app.descope.com/mcp-servers)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL" required>
Public URL of your FastMCP server (e.g., `https://your-server.com` or
`http://localhost:8000` for development)
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the Descope provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.descope.DescopeProvider
# Descope configuration
FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration
FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL=https://your-server.com
```
With environment variables set, your server code simplifies to:
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
# Authentication is automatically configured from environment
mcp = FastMCP(name="My Descope Protected Server")
# 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")
)
mcp = FastMCP(name="My Descope Protected Server", auth=auth)
```

View file

@ -182,74 +182,3 @@ Parameters (`jwt_signing_key` and `client_storage`) work together to ensure toke
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
</Note>
## Environment Variables
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the Discord provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.discord.DiscordProvider` to use Discord authentication.
</ParamField>
</Card>
### Discord-Specific Configuration
These environment variables provide default values for the Discord provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID" required>
Your Discord Application Client ID (e.g., `12345`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET" required>
Your Discord OAuth Client Secret
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_DISCORD_BASE_URL" default="http://localhost:8000">
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_DISCORD_ISSUER_URL" default="Uses BASE_URL">
Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_DISCORD_REDIRECT_PATH" default="/auth/callback">
Redirect path configured in your Discord OAuth settings
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_DISCORD_REQUIRED_SCOPES" default='["identify"]'>
Comma-, space-, or JSON-separated list of required Discord scopes (e.g., `identify,email` or `["identify","email"]`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_DISCORD_TIMEOUT_SECONDS" default="10">
HTTP request timeout for Discord API calls
</ParamField>
</Card>
Example `.env` file:
```bash
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.discord.DiscordProvider
FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID=12345
FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET=your-client-secret
FASTMCP_SERVER_AUTH_DISCORD_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_DISCORD_REQUIRED_SCOPES=identify,email
```
With environment variables set, your server code simplifies to:
```python server.py
from fastmcp import FastMCP
mcp = FastMCP(name="Discord Secured App")
@mcp.tool
async def protected_tool(query: str) -> str:
"""A tool that requires Discord authentication to access."""
return f"Processing authenticated request: {query}"
```

View file

@ -174,80 +174,3 @@ Parameters (`jwt_signing_key` and `client_storage`) work together to ensure toke
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
</Note>
## Environment Variables
<VersionBadge version="2.12.1" />
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the GitHub provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.github.GitHubProvider` to use GitHub authentication.
</ParamField>
</Card>
### GitHub-Specific Configuration
These environment variables provide default values for the GitHub provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID" required>
Your GitHub OAuth App Client ID (e.g., `Ov23liAbcDefGhiJkLmN`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET" required>
Your GitHub OAuth App Client Secret
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GITHUB_BASE_URL" default="http://localhost:8000">
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GITHUB_ISSUER_URL" default="Uses BASE_URL">
Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GITHUB_REDIRECT_PATH" default="/auth/callback">
Redirect path configured in your GitHub OAuth App
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GITHUB_REQUIRED_SCOPES" default='["user"]'>
Comma-, space-, or JSON-separated list of required GitHub scopes (e.g., `user repo` or `["user","repo"]`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GITHUB_TIMEOUT_SECONDS" default="10">
HTTP request timeout for GitHub API calls
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the GitHub provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
# GitHub OAuth credentials
FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=Ov23liAbcDefGhiJkLmN
FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET=github_pat_...
FASTMCP_SERVER_AUTH_GITHUB_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_GITHUB_REQUIRED_SCOPES=user,repo
```
With environment variables set, your server code simplifies to:
```python server.py
from fastmcp import FastMCP
# Authentication is automatically configured from environment
mcp = FastMCP(name="GitHub Secured App")
@mcp.tool
async def list_repos() -> list[str]:
"""List the authenticated user's repositories."""
# Your tool implementation here
pass
```

View file

@ -187,81 +187,4 @@ mcp = FastMCP(name="Production Google App", auth=auth_provider)
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>
## Environment Variables
<VersionBadge version="2.12.1" />
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the Google provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.google.GoogleProvider` to use Google authentication.
</ParamField>
</Card>
### Google-Specific Configuration
These environment variables provide default values for the Google provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID" required>
Your Google OAuth 2.0 Client ID (e.g., `123456789.apps.googleusercontent.com`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET" required>
Your Google OAuth 2.0 Client Secret (e.g., `GOCSPX-abc123...`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL" default="http://localhost:8000">
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GOOGLE_ISSUER_URL" default="Uses BASE_URL">
Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GOOGLE_REDIRECT_PATH" default="/auth/callback">
Redirect path configured in your Google OAuth Client
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES" default="[]">
Comma-, space-, or JSON-separated list of required Google scopes (e.g., `"openid,https://www.googleapis.com/auth/userinfo.email"` or `["openid", "https://www.googleapis.com/auth/userinfo.email"]`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_GOOGLE_TIMEOUT_SECONDS" default="10">
HTTP request timeout for Google API calls
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the Google provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.google.GoogleProvider
# Google OAuth credentials
FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com
FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-abc123...
FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES=openid,https://www.googleapis.com/auth/userinfo.email
```
With environment variables set, your server code simplifies to:
```python server.py
from fastmcp import FastMCP
# Authentication is automatically configured from environment
mcp = FastMCP(name="Google Secured App")
@mcp.tool
async def protected_tool(query: str) -> str:
"""A tool that requires Google authentication to access."""
# Your tool implementation here
return f"Processing authenticated request: {query}"
```
</Note>

View file

@ -17,7 +17,7 @@ This guide shows you how to secure your FastMCP server using **OCI IAM OAuth**.
### Prerequisites
1. An OCI cloud Account with access to create an Integrated Application in an Identity Domain.
2. Your FastMCP server's URL (For dev environments, it is http://localhost:8000. For PROD environments, it could be https://mcp.${DOMAIN}.com)
2. Your FastMCP server's URL (For dev environments, it is http://localhost:8000. For PROD environments, it could be https://mcp.yourdomain.com)
### Step 1: Make sure client access is enabled for JWK's URL
@ -88,8 +88,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. Update .env file and replace FASTMCP_SERVER_AUTH_OCI_CLIENT_ID and FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET values.
FASTMCP_SERVER_AUTH_OCI_IAM_GUID in the env file is the Identity domain URL that you chose for the MCP server.
Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in your code.
</Step>
</Steps>
@ -99,22 +98,20 @@ This is all you need to implement MCP server authentication against OCI IAM. How
Token exchange helps you exchange a logged-in user's OCI IAM token for an OCI control plane session token, also known as UPST (User Principal Session Token). To learn more about token exchange, refer to my [Workload Identity Federation Blog](https://www.ateam-oracle.com/post/workload-identity-federation)
For token exchange, we need to configure Identity propagation trust. The blog above discusses setting up the trust using REST APIs. However, you can also use OCI CLI. Before using the CLI command below, ensure that you have created a token exchange OAuth client. In most cases, you can use the same OAuth client that you created above. You will use the client ID of the token exchange OAuth client in the CLI command below and replace it with {FASTMCP_SERVER_AUTH_OCI_CLIENT_ID}.
You will also need to update the client secret for the token exchange OAuth client in the .env file. It is the FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET parameter. Update FASTMCP_SERVER_AUTH_OCI_IAM_GUID and FASTMCP_SERVER_AUTH_OCI_CLIENT_ID as well for the token exchange OAuth client in the .env file.
For token exchange, we need to configure Identity propagation trust. The blog above discusses setting up the trust using REST APIs. However, you can also use OCI CLI. Before using the CLI command below, ensure that you have created a token exchange OAuth client. In most cases, you can use the same OAuth client that you created above. Replace `<IAM_GUID>` and `<CLIENT_ID>` in the CLI command below with your actual values.
```bash
oci identity-domains identity-propagation-trust create \
--schemas '["urn:ietf:params:scim:schemas:oracle:idcs:IdentityPropagationTrust"]' \
--public-key-endpoint "https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oraclecloud.com/admin/v1/SigningCert/jwk" \
--public-key-endpoint "https://<IAM_GUID>.identity.oraclecloud.com/admin/v1/SigningCert/jwk" \
--name "For Token Exchange" --type "JWT" \
--issuer "https://identity.oraclecloud.com/" --active true \
--endpoint "https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oracleclcoud.com" \
--endpoint "https://<IAM_GUID>.identity.oraclecloud.com" \
--subject-claim-name "sub" --allow-impersonation false \
--subject-mapping-attribute "username" \
--subject-type "User" --client-claim-name "iss" \
--client-claim-values '["https://identity.oraclecloud.com/"]' \
--oauth-clients '["{FASTMCP_SERVER_AUTH_OCI_CLIENT_ID}"]'
--oauth-clients '["<CLIENT_ID>"]'
```
To exchange access token for OCI token and create a signer object, you need to add below code in MCP server. You can then use the signer object to create any OCI control plane client.
@ -129,9 +126,9 @@ import os
logger = get_logger(__name__)
# Load configuration from environment
FASTMCP_SERVER_AUTH_OCI_IAM_GUID = os.environ["FASTMCP_SERVER_AUTH_OCI_IAM_GUID"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_ID = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_ID"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET"]
OCI_IAM_GUID = os.environ.get("OCI_IAM_GUID")
OCI_CLIENT_ID = os.environ.get("OCI_CLIENT_ID")
OCI_CLIENT_SECRET = os.environ.get("OCI_CLIENT_SECRET")
_global_token_cache = {} #In memory cache for OCI session token signer
@ -152,9 +149,9 @@ def get_oci_signer() -> TokenExchangeSigner:
logger.debug(f"Creating new signer for token ID: {tokenID}")
signer = TokenExchangeSigner(
jwt_or_func=token,
oci_domain_id=FASTMCP_SERVER_AUTH_OCI_IAM_GUID.split(".")[0],
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID,
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET,
oci_domain_id=OCI_IAM_GUID.split(".")[0] if OCI_IAM_GUID else "",
client_id=OCI_CLIENT_ID,
client_secret=OCI_CLIENT_SECRET,
)
logger.debug(f"Signer {signer} created for token ID: {tokenID}")
@ -220,16 +217,12 @@ from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Load configuration from environment
FASTMCP_SERVER_AUTH_OCI_CONFIG_URL = os.environ["FASTMCP_SERVER_AUTH_OCI_CONFIG_URL"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_ID = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_ID"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET"]
# Production setup with encrypted persistent token storage
auth_provider = OCIProvider(
config_url=FASTMCP_SERVER_AUTH_OCI_CONFIG_URL,
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID,
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET,
base_url="https://your-production-domain.com",
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"],
@ -253,81 +246,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>
## Environment Variables
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the OCI provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.oci.OCIProvider` to use OCI IAM authentication.
</ParamField>
</Card>
### OCI-Specific Configuration
These environment variables provide default values for the OCI IAM provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_OCI_IAM_GUID" required>
Your OCI Application Configuration URL (e.g., `idcs-asdascxasd11......identity.oraclecloud.com`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_OCI_CONFIG_URL" required>
Your OCI Application Configuration URL (e.g., `https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oraclecloud.com/.well-known/openid-configuration`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_OCI_CLIENT_ID" required>
Your OCI Application Client ID (e.g., `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET" required>
Your OCI Application Client Secret (e.g., `idcsssvPYqbjemq...`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_OCI_BASE_URL" required>
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_OCI_REDIRECT_PATH" default="/auth/callback">
Redirect path configured in your OCI IAM Integrated Application
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the OCI IAM provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.oci.OCIProvider
# OCI IAM configuration and credentials
FASTMCP_SERVER_AUTH_OCI_IAM_GUID=idcs-asaacasd1111.....
FASTMCP_SERVER_AUTH_OCI_CONFIG_URL=https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oraclecloud.com/.well-known/openid-configuration
FASTMCP_SERVER_AUTH_OCI_CLIENT_ID=<your-client-id>
FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET=<your-client-secret>
FASTMCP_SERVER_AUTH_OCI_BASE_URL=https://your-server.com
```
With environment variables set, your server code simplifies to:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_access_token
# Authentication is automatically configured from environment
mcp = FastMCP(name="OCI Secured App")
@mcp.tool
def whoami() -> str:
"""The whoami function is to test MCP server without requiring token exchange.
This tool can be used to test successful authentication against OCI IAM.
It will return logged in user's subject (username from IAM domain)."""
token = get_access_token()
user = token.claims.get("sub")
return f"You are User: {user}"
```
</Info>

View file

@ -89,61 +89,23 @@ uv run python server.py
Use any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running serve. Verify that authentication succeeds and requests are authorized as expected.
### Provider selection
## Production Configuration
Setting this environment variable allows the Scalekit provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.scalekit.ScalekitProvider` to use Scalekit authentication.
</ParamField>
</Card>
### Scalekit-specific configuration
These environment variables provide default values for the Scalekit provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL" required>
Your Scalekit environment URL from the Admin Portal (e.g., `https://your-env.scalekit.com`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID" required>
Your Scalekit resource server ID from the MCP Servers section
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_BASE_URL" required>
Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000/` for development)
</ParamField>
Legacy `FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL` is still recognized for backward compatibility but will be removed soon-rename it to `...BASE_URL`.
<ParamField path="FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_REQUIRED_SCOPES" default="[]">
Comma-, space-, or JSON-separated list of scopes that tokens must include to access your server
</ParamField>
</Card>
Example `.env`:
```bash
# Use the Scalekit provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.scalekit.ScalekitProvider
# Scalekit configuration
FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL=https://your-env.scalekit.com
FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID=res_456
FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_BASE_URL=https://your-server.com/
# FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_REQUIRED_SCOPES=read,write
# FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL=https://your-server.com/ # Deprecated
```
With environment variables set, your server code simplifies to:
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
# Authentication is automatically configured from environment
mcp = FastMCP(name="My Scalekit Protected Server")
# 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")
)
mcp = FastMCP(name="My Scalekit Protected Server", auth=auth)
@mcp.tool
def protected_action() -> str:

View file

@ -90,53 +90,22 @@ When you run the client for the first time:
2. After you authorize, you'll be redirected back
3. The client receives the token and can make authenticated requests
## Environment Variables
## Production Configuration
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the Supabase provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.supabase.SupabaseProvider` to use Supabase authentication.
</ParamField>
</Card>
### Supabase-Specific Configuration
These environment variables provide default values for the Supabase provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL" required>
Your Supabase project URL (e.g., `https://abc123.supabase.co`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL" required>
Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000` for development)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_SUPABASE_REQUIRED_SCOPES" default="[]">
Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid email` or `["openid", "email"]`)
</ParamField>
</Card>
Example `.env` file:
```bash
# Use the Supabase provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.supabase.SupabaseProvider
# Supabase configuration
FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL=https://abc123.supabase.co
FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_SUPABASE_REQUIRED_SCOPES=openid,email
```
With environment variables set, your server code simplifies to:
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
# Load configuration from environment variables
auth = SupabaseProvider(
project_url=os.environ.get("SUPABASE_PROJECT_URL"),
base_url=os.environ.get("BASE_URL", "https://your-server.com")
)
mcp = FastMCP(name="Supabase Secured App", auth=auth)
# Authentication is automatically configured from environment
mcp = FastMCP(name="Supabase Protected Server")

View file

@ -168,93 +168,6 @@ Parameters (`jwt_signing_key` and `client_storage`) work together to ensure toke
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
</Note>
## Environment Variables
<VersionBadge version="2.12.1" />
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
Setting this environment variable allows the WorkOS provider to be used automatically without explicitly instantiating it in code.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH" default="Not set">
Set to `fastmcp.server.auth.providers.workos.WorkOSProvider` to use WorkOS authentication.
</ParamField>
</Card>
### WorkOS-Specific Configuration
These environment variables provide default values for the WorkOS provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
<Card>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID" required>
Your WorkOS OAuth App Client ID (e.g., `client_01K33Y6GGS7T3AWMPJWKW42Y3Q`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET" required>
Your WorkOS OAuth App Client Secret
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN" required>
Your WorkOS AuthKit domain (e.g., `https://your-app.authkit.app`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_BASE_URL" default="http://localhost:8000">
Public URL where OAuth endpoints will be accessible (includes any mount path)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_ISSUER_URL" default="Uses BASE_URL">
Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_REDIRECT_PATH" default="/auth/callback">
Redirect path configured in your WorkOS OAuth App
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES" default="[]">
Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid profile email` or `["openid","profile","email"]`)
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_WORKOS_TIMEOUT_SECONDS" default="10">
HTTP request timeout for WorkOS API calls
</ParamField>
</Card>
Example `.env` file:
```bash
# WorkOS OAuth credentials (always used as defaults)
FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID=client_01K33Y6GGS7T3AWMPJWKW42Y3Q
FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET=your_client_secret
FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN=https://your-app.authkit.app
FASTMCP_SERVER_AUTH_WORKOS_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES=["openid","profile","email"]
# Optional: Automatically provision WorkOS auth for all servers
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.WorkOSProvider
```
With environment variables set, you can either:
**Option 1: Manual instantiation (env vars provide defaults)**
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import WorkOSProvider
# Env vars provide default values for WorkOSProvider()
auth = WorkOSProvider() # Uses env var defaults
mcp = FastMCP(name="WorkOS Protected Server", auth=auth)
```
**Option 2: Automatic provisioning (requires FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.WorkOSProvider)**
```python server.py
from fastmcp import FastMCP
# Auth is automatically provisioned from FASTMCP_SERVER_AUTH
mcp = FastMCP(name="WorkOS Protected Server")
```
## Configuration Options
<Card>

View file

@ -31,12 +31,6 @@ Example:
## Classes
### `Auth0ProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/auth0.py#L37" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for Auth0 OIDC provider.
### `Auth0Provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/auth0.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -31,12 +31,6 @@ Example:
## Classes
### `AWSCognitoProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for AWS Cognito OAuth provider.
### `AWSCognitoTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -14,12 +14,6 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
## Classes
### `AzureProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for Azure OAuth provider.
### `AzureProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -15,8 +15,6 @@ for seamless MCP client authentication.
## Classes
### `DescopeProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/descope.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `DescopeProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/descope.py#L47" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -29,12 +29,6 @@ Example:
## Classes
### `DiscordProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/discord.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for Discord OAuth provider.
### `DiscordTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/discord.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -29,12 +29,6 @@ Example:
## Classes
### `GitHubProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L40" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for GitHub OAuth provider.
### `GitHubTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L65" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -29,12 +29,6 @@ Example:
## Classes
### `GoogleProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for Google OAuth provider.
### `GoogleTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -31,12 +31,6 @@ Example:
## Classes
### `IntrospectionTokenVerifierSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/introspection.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for OAuth 2.0 Token Introspection verification.
### `IntrospectionTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/introspection.py#L65" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -60,12 +60,6 @@ Generate a test JWT token for testing purposes.
- `kid`: Key ID to include in header
### `JWTVerifierSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L142" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for JWT token verification.
### `JWTVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L165" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -27,22 +27,22 @@ Example:
import os
# Load configuration from environment
FASTMCP_SERVER_AUTH_OCI_CONFIG_URL = os.environ["FASTMCP_SERVER_AUTH_OCI_CONFIG_URL"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_ID = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_ID"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET"]
FASTMCP_SERVER_AUTH_OCI_IAM_GUID = os.environ["FASTMCP_SERVER_AUTH_OCI_IAM_GUID"]
import oci
from oci.auth.signers import TokenExchangeSigner
logger = get_logger(__name__)
# Load configuration from environment
OCI_CONFIG_URL = os.environ.get("OCI_CONFIG_URL")
OCI_CLIENT_ID = os.environ.get("OCI_CLIENT_ID")
OCI_CLIENT_SECRET = os.environ.get("OCI_CLIENT_SECRET")
OCI_IAM_GUID = os.environ.get("OCI_IAM_GUID")
# Simple OCI OIDC protection
auth = OCIProvider(
config_url=FASTMCP_SERVER_AUTH_OCI_CONFIG_URL, #config URL is the OCI IAM Domain OIDC discovery URL.
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET, #This is same as the client secret configured for the OCI IAM Domain Integrated Application
config_url=OCI_CONFIG_URL, # config URL is the OCI IAM Domain OIDC discovery URL
client_id=OCI_CLIENT_ID, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=OCI_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",
@ -50,7 +50,7 @@ Example:
# 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
_global_token_cache = {} # In memory cache for OCI session token signer
def get_oci_signer() -> TokenExchangeSigner:
@ -58,20 +58,20 @@ Example:
tokenID = authntoken.claims.get("jti")
token = authntoken.token
#Check if the signer exists for the token ID in memory cache
# 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
# 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=FASTMCP_SERVER_AUTH_OCI_IAM_GUID.split(".")[0], #This is same as IAM GUID configured for the OCI IAM Domain
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET #This is same as the client secret configured for the OCI IAM Domain Integrated Application
oci_domain_id=OCI_IAM_GUID.split(".")[0] if OCI_IAM_GUID else "", # This is same as IAM GUID configured for the OCI IAM Domain
client_id=OCI_CLIENT_ID, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=OCI_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}")
@ -87,12 +87,6 @@ Example:
## Classes
### `OCIProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/oci.py#L93" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for OCI IAM domain OIDC provider.
### `OCIProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/oci.py#L119" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -15,8 +15,6 @@ authentication for seamless MCP client authentication.
## Classes
### `ScalekitProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ScalekitProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -15,8 +15,6 @@ for seamless MCP client authentication.
## Classes
### `SupabaseProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SupabaseProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -18,12 +18,6 @@ Choose based on your WorkOS setup and authentication requirements.
## Classes
### `WorkOSProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L31" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Settings for WorkOS OAuth provider.
### `WorkOSTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
@ -65,8 +59,6 @@ Setup Requirements:
4. Note your Client ID and Client Secret
### `AuthKitProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L287" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AuthKitProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L304" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -48,9 +48,3 @@ treated as a nested setting.
```python
normalize_log_level(cls, v)
```
#### `server_auth_class` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L396" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
server_auth_class(self) -> AuthProvider | None
```

View file

@ -178,78 +178,28 @@ This example shows the basic structure of a custom OAuth provider. The actual im
→ **Complete guide**: [Full OAuth Server](/servers/auth/full-oauth-server)
## Configuration Approaches
## Configuration
FastMCP supports both programmatic configuration for maximum flexibility and environment-based configuration for deployment simplicity.
Authentication providers are configured programmatically by instantiating them directly in your code with their required parameters. This makes dependencies explicit and allows your IDE to provide helpful autocompletion and type checking.
### Programmatic Configuration
Programmatic configuration provides complete control over authentication settings and allows for complex initialization logic. This approach works well during development and when you need to customize authentication behavior based on runtime conditions.
Authentication providers are instantiated directly in your code with their required parameters. This makes dependencies explicit and allows your IDE to provide helpful autocompletion and type checking.
### Environment Configuration
<VersionBadge version="2.12.1" />
Environment-based configuration separates authentication settings from application code, enabling the same codebase to work across different deployment environments without modification.
FastMCP automatically detects authentication configuration from environment variables when no explicit `auth` parameter is provided. The configuration system supports all authentication providers and their various options.
#### Provider Configuration
Authentication providers are configured by specifying the full module path to the provider class:
<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.auth.providers.jwt.JWTVerifier` - JWT token verification
- `fastmcp.server.auth.providers.workos.WorkOSProvider` - WorkOS OAuth
- `fastmcp.server.auth.providers.workos.AuthKitProvider` - WorkOS AuthKit
- `mycompany.auth.CustomProvider` - Your custom provider class
</ParamField>
When using providers like GitHub or Google, you'll need to set provider-specific environment variables:
```bash
# GitHub OAuth
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.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_GOOGLE_CLIENT_ID="123456.apps.googleusercontent.com"
export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET="GOCSPX-..."
```
#### Provider-Specific Configuration
Each provider has its own configuration options set through environment variables:
```bash
# JWT Token Verification
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.jwt.JWTVerifier
export FASTMCP_SERVER_AUTH_JWT_JWKS_URI="https://auth.example.com/jwks"
export FASTMCP_SERVER_AUTH_JWT_ISSUER="https://auth.example.com"
export FASTMCP_SERVER_AUTH_JWT_AUDIENCE="mcp-server"
# Custom Provider
export FASTMCP_SERVER_AUTH=mycompany.auth.CustomProvider
# Plus any environment variables your custom provider expects
```
With these environment variables set, creating an authenticated FastMCP server requires no additional configuration:
For production deployments, load sensitive values like client secrets from environment variables:
```python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
# Authentication automatically configured from environment
mcp = FastMCP(name="My Server")
# Load secrets from environment variables
auth = GitHubProvider(
client_id=os.environ.get("GITHUB_CLIENT_ID"),
client_secret=os.environ.get("GITHUB_CLIENT_SECRET"),
base_url=os.environ.get("BASE_URL", "http://localhost:8000")
)
mcp = FastMCP(name="My Server", auth=auth)
```
This approach simplifies deployment pipelines and follows twelve-factor app principles for configuration management.
This approach keeps secrets out of your codebase while maintaining explicit configuration. You can use any environment variable names you prefer - there are no special prefixes required.
## Choosing Your Implementation

View file

@ -560,29 +560,23 @@ The consent page automatically displays your server's name, icon, and website UR
- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance
- [Confused Deputy Attacks Explained](https://den.dev/blog/mcp-confused-deputy-api-management/) - Detailed walkthrough by Den Delimarsky
## Environment Configuration
## Production Configuration
<VersionBadge version="2.12.1" />
For production deployments, configure the OAuth proxy through environment variables instead of hardcoding credentials:
```bash
# Specify the provider implementation
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
# Provider-specific credentials
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID="Ov23li..."
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET="abc123..."
export FASTMCP_SERVER_AUTH_GITHUB_BASE_URL="https://your-production-server.com"
```
With environment configuration, your server code simplifies to:
For production deployments, load sensitive credentials from environment variables:
```python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
# Authentication automatically configured from environment
mcp = FastMCP(name="My Server")
# Load secrets from environment variables
auth = GitHubProvider(
client_id=os.environ.get("GITHUB_CLIENT_ID"),
client_secret=os.environ.get("GITHUB_CLIENT_SECRET"),
base_url=os.environ.get("BASE_URL", "https://your-production-server.com")
)
mcp = FastMCP(name="My Server", auth=auth)
@mcp.tool
def protected_tool(data: str) -> str:
@ -592,3 +586,5 @@ def protected_tool(data: str) -> str:
if __name__ == "__main__":
mcp.run(transport="http", port=8000)
```
This keeps secrets out of your codebase while maintaining explicit configuration.

View file

@ -233,31 +233,25 @@ OAuth scopes are configured with `required_scopes` to automatically request the
Dynamic clients created by the proxy will automatically include these scopes in their authorization requests.
## Environment Configuration
## Production Configuration
<VersionBadge version="2.13.0" />
For production deployments, configure the OIDC proxy through environment variables instead of hardcoding credentials:
```bash
# Specify the provider implementation
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider
# Provider-specific credentials
export FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration
export FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID=tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB
export FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET=vPYqbjemq...
export FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE=https://...
export FASTMCP_SERVER_AUTH_AUTH0_BASE_URL=https://localhost:8000
```
With environment configuration, your server code simplifies to:
For production deployments, load sensitive credentials from environment variables:
```python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
# Authentication automatically configured from environment
mcp = FastMCP(name="My Server")
# Load secrets from environment variables
auth = Auth0Provider(
config_url=os.environ.get("AUTH0_CONFIG_URL"),
client_id=os.environ.get("AUTH0_CLIENT_ID"),
client_secret=os.environ.get("AUTH0_CLIENT_SECRET"),
audience=os.environ.get("AUTH0_AUDIENCE"),
base_url=os.environ.get("BASE_URL", "https://localhost:8000")
)
mcp = FastMCP(name="My Server", auth=auth)
@mcp.tool
def protected_tool(data: str) -> str:
@ -267,3 +261,5 @@ def protected_tool(data: str) -> str:
if __name__ == "__main__":
mcp.run(transport="http", port=8000)
```
This keeps secrets out of your codebase while maintaining explicit configuration.

View file

@ -303,39 +303,31 @@ print(f"Test token: {test_token}")
This pattern enables comprehensive testing of JWT validation logic without depending on external token issuers. The generated tokens are cryptographically valid and will pass all standard JWT validation checks.
## Environment Configuration
## Production Configuration
<VersionBadge version="2.12.1" />
FastMCP supports both programmatic and environment-based configuration for token verification, enabling flexible deployment across different environments.
Environment-based configuration separates authentication settings from application code, following twelve-factor app principles and simplifying deployment pipelines.
```bash
# Enable JWT verification
export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.jwt.JWTVerifier
# For asymmetric verification with JWKS endpoint:
export FASTMCP_SERVER_AUTH_JWT_JWKS_URI="https://auth.company.com/.well-known/jwks.json"
export FASTMCP_SERVER_AUTH_JWT_ISSUER="https://auth.company.com"
export FASTMCP_SERVER_AUTH_JWT_AUDIENCE="mcp-production-api"
export FASTMCP_SERVER_AUTH_JWT_REQUIRED_SCOPES="read:data,write:data"
# OR for symmetric key verification (HMAC):
export FASTMCP_SERVER_AUTH_JWT_PUBLIC_KEY="your-shared-secret-key-minimum-32-chars"
export FASTMCP_SERVER_AUTH_JWT_ALGORITHM="HS256" # or HS384, HS512
export FASTMCP_SERVER_AUTH_JWT_ISSUER="internal-auth-service"
export FASTMCP_SERVER_AUTH_JWT_AUDIENCE="mcp-internal-api"
```
With these environment variables configured, your FastMCP server automatically enables JWT verification:
For production deployments, load sensitive configuration from environment variables:
```python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
# Authentication automatically configured from environment
mcp = FastMCP(name="Production API")
# Load configuration from environment variables
# Parse comma-separated scopes if provided
scopes_env = os.environ.get("JWT_REQUIRED_SCOPES")
required_scopes = scopes_env.split(",") if scopes_env else None
verifier = JWTVerifier(
jwks_uri=os.environ.get("JWT_JWKS_URI"),
issuer=os.environ.get("JWT_ISSUER"),
audience=os.environ.get("JWT_AUDIENCE"),
required_scopes=required_scopes,
)
mcp = FastMCP(name="Production API", auth=verifier)
```
This keeps configuration out of your codebase while maintaining explicit setup.
This approach enables the same codebase to run across development, staging, and production environments with different authentication requirements. Development might use static tokens while production uses JWT verification, all controlled through environment configuration.

View file

@ -22,44 +22,15 @@ Example:
"""
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class Auth0ProviderSettings(BaseSettings):
"""Settings for Auth0 OIDC provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_AUTH0_",
env_file=ENV_FILE,
extra="ignore",
)
config_url: AnyHttpUrl | None = None
client_id: str | None = None
client_secret: SecretStr | None = None
audience: str | None = None
base_url: AnyHttpUrl | None = None
issuer_url: AnyHttpUrl | 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
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class Auth0Provider(OIDCProxy):
"""An Auth0 provider implementation for FastMCP.
@ -87,17 +58,17 @@ class Auth0Provider(OIDCProxy):
def __init__(
self,
*,
config_url: AnyHttpUrl | str | NotSetT = NotSet,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
audience: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
config_url: AnyHttpUrl | str,
client_id: str,
client_secret: str,
audience: str,
base_url: AnyHttpUrl | str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
) -> None:
"""Initialize Auth0 OAuth provider.
@ -125,69 +96,28 @@ class Auth0Provider(OIDCProxy):
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
"""
settings = Auth0ProviderSettings.model_validate(
{
k: v
for k, v in {
"config_url": config_url,
"client_id": client_id,
"client_secret": client_secret,
"audience": audience,
"base_url": base_url,
"issuer_url": issuer_url,
"required_scopes": required_scopes,
"redirect_path": redirect_path,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
}.items()
if v is not NotSet
}
# Parse scopes if provided as string
auth0_required_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
if not settings.config_url:
raise ValueError(
"config_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL"
)
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET"
)
if not settings.audience:
raise ValueError(
"audience is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE"
)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_BASE_URL"
)
auth0_required_scopes = settings.required_scopes or ["openid"]
super().__init__(
config_url=settings.config_url,
client_id=settings.client_id,
client_secret=settings.client_secret.get_secret_value(),
audience=settings.audience,
base_url=settings.base_url,
issuer_url=settings.issuer_url,
redirect_path=settings.redirect_path,
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
audience=audience,
base_url=base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
required_scopes=auth0_required_scopes,
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
)
logger.debug(
"Initialized Auth0 OAuth provider for client %s with scopes: %s",
settings.client_id,
client_id,
auth0_required_scopes,
)

View file

@ -24,47 +24,18 @@ Example:
from __future__ import annotations
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl
from fastmcp.server.auth import TokenVerifier
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class AWSCognitoProviderSettings(BaseSettings):
"""Settings for AWS Cognito OAuth provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_AWS_COGNITO_",
env_file=ENV_FILE,
extra="ignore",
)
user_pool_id: str | None = None
aws_region: str | None = None
client_id: str | None = None
client_secret: SecretStr | None = None
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
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class AWSCognitoTokenVerifier(JWTVerifier):
"""Token verifier that filters claims to Cognito-specific subset."""
@ -126,27 +97,27 @@ class AWSCognitoProvider(OIDCProxy):
def __init__(
self,
*,
user_pool_id: str | NotSetT = NotSet,
aws_region: str | NotSetT = NotSet,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
user_pool_id: str,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
):
"""Initialize AWS Cognito OAuth provider.
Args:
user_pool_id: Your Cognito User Pool ID (e.g., "eu-central-1_XXXXXXXXX")
aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
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)
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")
@ -164,81 +135,37 @@ class AWSCognitoProvider(OIDCProxy):
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
"""
settings = AWSCognitoProviderSettings.model_validate(
{
k: v
for k, v in {
"user_pool_id": user_pool_id,
"aws_region": aws_region,
"client_id": client_id,
"client_secret": client_secret,
"base_url": base_url,
"issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
}.items()
if v is not NotSet
}
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
# Validate required settings
if not settings.user_pool_id:
raise ValueError(
"user_pool_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID"
)
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET"
)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL"
)
# Apply defaults
required_scopes_final = settings.required_scopes or ["openid"]
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
aws_region_final = settings.aws_region or "eu-central-1"
redirect_path_final = settings.redirect_path or "/auth/callback"
# Construct OIDC discovery URL
config_url = f"https://cognito-idp.{aws_region_final}.amazonaws.com/{settings.user_pool_id}/.well-known/openid-configuration"
# Extract secret string from SecretStr
client_secret_str = (
settings.client_secret.get_secret_value() if settings.client_secret else ""
)
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 = settings.user_pool_id
self.aws_region = aws_region_final
self.user_pool_id = user_pool_id
self.aws_region = aws_region
# Initialize OIDC proxy with Cognito discovery
super().__init__(
config_url=config_url,
client_id=settings.client_id,
client_secret=client_secret_str,
client_id=client_id,
client_secret=client_secret,
algorithm="RS256",
required_scopes=required_scopes_final,
base_url=settings.base_url,
issuer_url=settings.issuer_url,
redirect_path=redirect_path_final,
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
base_url=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=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
)
logger.debug(
"Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
settings.client_id,
client_id,
required_scopes_final,
)

View file

@ -9,15 +9,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from key_value.aio.protocols import AsyncKeyValue
from pydantic import SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
if TYPE_CHECKING:
from mcp.server.auth.provider import AuthorizationParams
@ -31,39 +27,6 @@ logger = get_logger(__name__)
OIDC_SCOPES = frozenset({"openid", "profile", "email", "offline_access"})
class AzureProviderSettings(BaseSettings):
"""Settings for Azure OAuth provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
env_file=ENV_FILE,
extra="ignore",
)
client_id: str | None = None
client_secret: SecretStr | None = None
tenant_id: str | None = None
identifier_uri: str | None = None
base_url: str | None = None
issuer_url: str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
additional_authorize_scopes: list[str] | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
base_authority: str = "login.microsoftonline.com"
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v: object) -> list[str] | None:
return parse_scopes(v)
@field_validator("additional_authorize_scopes", mode="before")
@classmethod
def _parse_additional_authorize_scopes(cls, v: object) -> list[str] | None:
return parse_scopes(v)
class AzureProvider(OAuthProxy):
"""Azure (Microsoft Entra) OAuth provider for FastMCP.
@ -127,20 +90,20 @@ class AzureProvider(OAuthProxy):
def __init__(
self,
*,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
tenant_id: str | NotSetT = NotSet,
identifier_uri: str | NotSetT | None = NotSet,
base_url: str | NotSetT = NotSet,
issuer_url: str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
additional_authorize_scopes: list[str] | NotSetT | None = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
client_id: str,
client_secret: str,
tenant_id: str,
required_scopes: list[str],
base_url: str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
base_authority: str | NotSetT = NotSet,
base_authority: str = "login.microsoftonline.com",
) -> None:
"""Initialize Azure OAuth provider.
@ -185,131 +148,75 @@ class AzureProvider(OAuthProxy):
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
"""
settings = AzureProviderSettings.model_validate(
{
k: v
for k, v in {
"client_id": client_id,
"client_secret": client_secret,
"tenant_id": tenant_id,
"identifier_uri": identifier_uri,
"base_url": base_url,
"issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"additional_authorize_scopes": additional_authorize_scopes,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
"base_authority": base_authority,
}.items()
if v is not NotSet
}
# Parse scopes if provided as string
parsed_required_scopes = parse_scopes(required_scopes)
parsed_additional_scopes = (
parse_scopes(additional_authorize_scopes)
if additional_authorize_scopes
else []
)
# Validate required settings
if not settings.client_id:
msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
raise ValueError(msg)
if not settings.client_secret:
msg = "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
raise ValueError(msg)
# Validate tenant_id is provided
if not settings.tenant_id:
msg = (
"tenant_id is required - set via parameter or "
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. Use your Azure tenant ID "
"(found in Azure Portal), 'organizations', or 'consumers'"
)
raise ValueError(msg)
# Validate required_scopes has at least one scope
if not settings.required_scopes:
msg = (
"required_scopes must include at least one scope - set via parameter or "
"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES. Azure's OAuth API requires "
"the 'scope' parameter in authorization requests. Use the unprefixed scope "
"names from your Azure App registration (e.g., ['read', 'write'])"
)
raise ValueError(msg)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_BASE_URL"
)
# Apply defaults
self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
self.additional_authorize_scopes = settings.additional_authorize_scopes or []
tenant_id_final = settings.tenant_id
self.identifier_uri = identifier_uri or f"api://{client_id}"
self.additional_authorize_scopes = parsed_additional_scopes
# Always validate tokens against the app's API client ID using JWT
base_authority_final = settings.base_authority
issuer = f"https://{base_authority_final}/{tenant_id_final}/v2.0"
jwks_uri = (
f"https://{base_authority_final}/{tenant_id_final}/discovery/v2.0/keys"
)
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 = None
if settings.required_scopes:
if parsed_required_scopes:
validation_scopes = [
s for s in settings.required_scopes if s not in OIDC_SCOPES
s for s in parsed_required_scopes if s not in OIDC_SCOPES
]
# If all scopes were OIDC scopes, use None (no scope validation)
if not validation_scopes:
validation_scopes = None
else:
validation_scopes = None
token_verifier = JWTVerifier(
jwks_uri=jwks_uri,
issuer=issuer,
audience=settings.client_id,
audience=client_id,
algorithm="RS256",
required_scopes=validation_scopes, # Only validate non-OIDC scopes
)
# Extract secret string from SecretStr
client_secret_str = (
settings.client_secret.get_secret_value() if settings.client_secret else ""
)
# Build Azure OAuth endpoints with tenant
authorization_endpoint = (
f"https://{base_authority_final}/{tenant_id_final}/oauth2/v2.0/authorize"
)
token_endpoint = (
f"https://{base_authority_final}/{tenant_id_final}/oauth2/v2.0/token"
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
super().__init__(
upstream_authorization_endpoint=authorization_endpoint,
upstream_token_endpoint=token_endpoint,
upstream_client_id=settings.client_id,
upstream_client_secret=client_secret_str,
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
issuer_url=settings.issuer_url
or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
base_url=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=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
# Advertise full scopes including OIDC (even though we only validate non-OIDC)
valid_scopes=settings.required_scopes,
valid_scopes=parsed_required_scopes,
)
authority_info = ""
if base_authority_final != "login.microsoftonline.com":
authority_info = f" using authority {base_authority_final}"
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",
settings.client_id,
tenant_id_final,
client_id,
tenant_id,
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
authority_info,
)

View file

@ -10,40 +10,18 @@ from __future__ import annotations
from urllib.parse import urlparse
import httpx
from pydantic import AnyHttpUrl, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class DescopeProviderSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_",
env_file=ENV_FILE,
extra="ignore",
)
config_url: AnyHttpUrl | None = None
project_id: str | None = None
descope_base_url: AnyHttpUrl | str | None = None
base_url: AnyHttpUrl
required_scopes: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class DescopeProvider(RemoteAuthProvider):
"""Descope metadata provider for DCR (Dynamic Client Registration).
@ -85,46 +63,37 @@ class DescopeProvider(RemoteAuthProvider):
def __init__(
self,
*,
config_url: AnyHttpUrl | str | NotSetT = NotSet,
project_id: str | NotSetT = NotSet,
descope_base_url: AnyHttpUrl | str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
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,
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.
base_url: Public URL of this FastMCP server
required_scopes: Optional list of scopes that must be present in validated tokens.
These scopes will be included in the protected resource metadata.
token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
"""
settings = DescopeProviderSettings.model_validate(
{
k: v
for k, v in {
"config_url": config_url,
"project_id": project_id,
"descope_base_url": descope_base_url,
"base_url": base_url,
"required_scopes": required_scopes,
}.items()
if v is not NotSet
}
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
)
self.base_url = AnyHttpUrl(str(settings.base_url).rstrip("/"))
# Determine which API is being used
if settings.config_url is not None:
if config_url is not None:
# New API: use config_url
# Strip /.well-known/openid-configuration from config_url if present
issuer_url = str(settings.config_url)
issuer_url = str(config_url)
if issuer_url.endswith("/.well-known/openid-configuration"):
issuer_url = issuer_url[: -len("/.well-known/openid-configuration")]
@ -150,10 +119,10 @@ class DescopeProvider(RemoteAuthProvider):
self.descope_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}".rstrip(
"/"
)
elif settings.project_id is not None and settings.descope_base_url is not None:
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 = settings.project_id
descope_base_url_str = str(settings.descope_base_url).rstrip("/")
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}"
@ -172,7 +141,7 @@ class DescopeProvider(RemoteAuthProvider):
issuer=issuer_url,
algorithm="RS256",
audience=self.project_id,
required_scopes=settings.required_scopes,
required_scopes=parsed_scopes,
)
# Initialize RemoteAuthProvider with Descope as the authorization server

View file

@ -26,45 +26,17 @@ from datetime import datetime
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class DiscordProviderSettings(BaseSettings):
"""Settings for Discord OAuth provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_DISCORD_",
env_file=ENV_FILE,
extra="ignore",
)
client_id: str | None = None
client_secret: SecretStr | None = None
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 | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class DiscordTokenVerifier(TokenVerifier):
"""Token verifier for Discord OAuth tokens.
@ -200,16 +172,16 @@ class DiscordProvider(OAuthProxy):
def __init__(
self,
*,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
):
"""Initialize Discord OAuth provider.
@ -225,7 +197,7 @@ class DiscordProvider(OAuthProxy):
- "identify" for profile info (default)
- "email" for email access
- "guilds" for server membership info
timeout_seconds: HTTP request timeout for Discord API calls
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).
@ -239,74 +211,37 @@ class DiscordProvider(OAuthProxy):
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
"""
settings = DiscordProviderSettings.model_validate(
{
k: v
for k, v in {
"client_id": client_id,
"client_secret": client_secret,
"base_url": base_url,
"issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
}.items()
if v is not NotSet
}
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes)
if required_scopes is not None
else ["identify"]
)
# Validate required settings
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET"
)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_DISCORD_BASE_URL"
)
# Apply defaults
timeout_seconds_final = settings.timeout_seconds or 10
required_scopes_final = settings.required_scopes or ["identify"]
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
# Create Discord token verifier
token_verifier = DiscordTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds_final,
)
# Extract secret string from SecretStr
client_secret_str = (
settings.client_secret.get_secret_value() if settings.client_secret else ""
timeout_seconds=timeout_seconds,
)
# 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=settings.client_id,
upstream_client_secret=client_secret_str,
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
issuer_url=settings.issuer_url
or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
base_url=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=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
)
logger.debug(
"Initialized Discord OAuth provider for client %s with scopes: %s",
settings.client_id,
client_id,
required_scopes_final,
)

View file

@ -23,45 +23,17 @@ from __future__ import annotations
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class GitHubProviderSettings(BaseSettings):
"""Settings for GitHub OAuth provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_GITHUB_",
env_file=ENV_FILE,
extra="ignore",
)
client_id: str | None = None
client_secret: SecretStr | None = None
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 | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class GitHubTokenVerifier(TokenVerifier):
"""Token verifier for GitHub OAuth tokens.
@ -198,16 +170,16 @@ class GitHubProvider(OAuthProxy):
def __init__(
self,
*,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
):
"""Initialize GitHub OAuth provider.
@ -220,7 +192,7 @@ class GitHubProvider(OAuthProxy):
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
timeout_seconds: HTTP request timeout for GitHub 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).
@ -234,75 +206,35 @@ class GitHubProvider(OAuthProxy):
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
"""
settings = GitHubProviderSettings.model_validate(
{
k: v
for k, v in {
"client_id": client_id,
"client_secret": client_secret,
"base_url": base_url,
"issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
}.items()
if v is not NotSet
}
# Parse scopes if provided as string
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["user"]
)
# Validate required settings
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET"
)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_BASE_URL"
)
# Apply defaults
timeout_seconds_final = settings.timeout_seconds or 10
required_scopes_final = settings.required_scopes or ["user"]
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
# Create GitHub token verifier
token_verifier = GitHubTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds_final,
)
# Extract secret string from SecretStr
client_secret_str = (
settings.client_secret.get_secret_value() if settings.client_secret else ""
timeout_seconds=timeout_seconds,
)
# 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=settings.client_id,
upstream_client_secret=client_secret_str,
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
issuer_url=settings.issuer_url
or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
base_url=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=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
)
logger.debug(
"Initialized GitHub OAuth provider for client %s with scopes: %s",
settings.client_id,
client_id,
required_scopes_final,
)

View file

@ -25,45 +25,17 @@ import time
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class GoogleProviderSettings(BaseSettings):
"""Settings for Google OAuth provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_GOOGLE_",
env_file=ENV_FILE,
extra="ignore",
)
client_id: str | None = None
client_secret: SecretStr | None = None
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 | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class GoogleTokenVerifier(TokenVerifier):
"""Token verifier for Google OAuth tokens.
@ -214,16 +186,16 @@ class GoogleProvider(OAuthProxy):
def __init__(
self,
*,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
extra_authorize_params: dict[str, str] | None = None,
):
@ -240,7 +212,7 @@ class GoogleProvider(OAuthProxy):
- "openid" for OpenID Connect (default)
- "https://www.googleapis.com/auth/userinfo.email" for email access
- "https://www.googleapis.com/auth/userinfo.profile" for profile info
timeout_seconds: HTTP request timeout for Google API calls
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).
@ -258,54 +230,16 @@ class GoogleProvider(OAuthProxy):
refresh tokens are returned. You can override these defaults or add additional parameters.
Example: {"prompt": "select_account"} to let users choose their Google account.
"""
settings = GoogleProviderSettings.model_validate(
{
k: v
for k, v in {
"client_id": client_id,
"client_secret": client_secret,
"base_url": base_url,
"issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
}.items()
if v is not NotSet
}
)
# Validate required settings
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET"
)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL"
)
# Apply defaults
timeout_seconds_final = settings.timeout_seconds or 10
# Parse scopes if provided as string
# Google requires at least one scope - openid is the minimal OIDC scope
required_scopes_final = settings.required_scopes or ["openid"]
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
required_scopes_final = (
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
)
# Create Google token verifier
token_verifier = GoogleTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds_final,
)
# Extract secret string from SecretStr
client_secret_str = (
settings.client_secret.get_secret_value() if settings.client_secret else ""
timeout_seconds=timeout_seconds,
)
# Set Google-specific defaults for extra authorize params
@ -324,22 +258,21 @@ class GoogleProvider(OAuthProxy):
super().__init__(
upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
upstream_token_endpoint="https://oauth2.googleapis.com/token",
upstream_client_id=settings.client_id,
upstream_client_secret=client_secret_str,
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
issuer_url=settings.issuer_url
or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
base_url=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=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
extra_authorize_params=extra_authorize_params_final,
)
logger.debug(
"Initialized Google OAuth provider for client %s with scopes: %s",
settings.client_id,
client_id,
required_scopes_final,
)

View file

@ -28,40 +28,15 @@ import time
from typing import Any
import httpx
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl, SecretStr
from fastmcp.server.auth import AccessToken, TokenVerifier
from fastmcp.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class IntrospectionTokenVerifierSettings(BaseSettings):
"""Settings for OAuth 2.0 Token Introspection verification."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_INTROSPECTION_",
env_file=ENV_FILE,
extra="ignore",
)
introspection_url: str | None = None
client_id: str | None = None
client_secret: SecretStr | None = None
timeout_seconds: int = 10
required_scopes: list[str] | None = None
base_url: AnyHttpUrl | str | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class IntrospectionTokenVerifier(TokenVerifier):
"""
OAuth 2.0 Token Introspection verifier (RFC 7662).
@ -93,12 +68,12 @@ class IntrospectionTokenVerifier(TokenVerifier):
def __init__(
self,
*,
introspection_url: str | NotSetT = NotSet,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
introspection_url: str,
client_id: str,
client_secret: str | SecretStr,
timeout_seconds: int = 10,
required_scopes: list[str] | None = None,
base_url: AnyHttpUrl | str | None = None,
):
"""
Initialize the introspection token verifier.
@ -111,45 +86,21 @@ class IntrospectionTokenVerifier(TokenVerifier):
required_scopes: Required scopes for all tokens (optional)
base_url: Base URL for TokenVerifier protocol
"""
settings = IntrospectionTokenVerifierSettings.model_validate(
{
k: v
for k, v in {
"introspection_url": introspection_url,
"client_id": client_id,
"client_secret": client_secret,
"timeout_seconds": timeout_seconds,
"required_scopes": required_scopes,
"base_url": base_url,
}.items()
if v is not NotSet
}
# Parse scopes if provided as string
parsed_required_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else None
)
if not settings.introspection_url:
raise ValueError(
"introspection_url is required - set via parameter or "
"FASTMCP_SERVER_AUTH_INTROSPECTION_INTROSPECTION_URL"
)
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or "
"FASTMCP_SERVER_AUTH_INTROSPECTION_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or "
"FASTMCP_SERVER_AUTH_INTROSPECTION_CLIENT_SECRET"
)
super().__init__(base_url=base_url, required_scopes=parsed_required_scopes)
super().__init__(
base_url=settings.base_url, required_scopes=settings.required_scopes
self.introspection_url = introspection_url
self.client_id = client_id
self.client_secret = (
client_secret.get_secret_value()
if isinstance(client_secret, SecretStr)
else client_secret
)
self.introspection_url = settings.introspection_url
self.client_id = settings.client_id
self.client_secret = settings.client_secret.get_secret_value()
self.timeout_seconds = settings.timeout_seconds
self.timeout_seconds = timeout_seconds
self.logger = get_logger(__name__)
def _create_basic_auth_header(self) -> str:

View file

@ -11,15 +11,12 @@ from authlib.jose import JsonWebKey, JsonWebToken
from authlib.jose.errors import JoseError
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl, SecretStr
from typing_extensions import TypedDict
from fastmcp.server.auth import AccessToken, TokenVerifier
from fastmcp.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
@ -139,29 +136,6 @@ class RSAKeyPair:
return token_bytes.decode("utf-8")
class JWTVerifierSettings(BaseSettings):
"""Settings for JWT token verification."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_JWT_",
env_file=ENV_FILE,
extra="ignore",
)
public_key: str | None = None
jwks_uri: str | None = None
issuer: str | list[str] | None = None
algorithm: str | None = None
audience: str | list[str] | None = None
required_scopes: list[str] | None = None
base_url: AnyHttpUrl | str | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class JWTVerifier(TokenVerifier):
"""
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
@ -184,52 +158,36 @@ class JWTVerifier(TokenVerifier):
def __init__(
self,
*,
public_key: str | NotSetT | None = NotSet,
jwks_uri: str | NotSetT | None = NotSet,
issuer: str | list[str] | NotSetT | None = NotSet,
audience: str | list[str] | NotSetT | None = NotSet,
algorithm: str | NotSetT | None = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
public_key: str | None = None,
jwks_uri: str | None = None,
issuer: str | list[str] | None = None,
audience: str | list[str] | None = None,
algorithm: str | None = None,
required_scopes: list[str] | None = None,
base_url: AnyHttpUrl | str | None = None,
):
"""
Initialize a JWTVerifier configured to validate JWTs using either a static key or a JWKS endpoint.
Parameters:
public_key (str | NotSetT | None): PEM-encoded public key for asymmetric algorithms or shared secret for symmetric algorithms.
jwks_uri (str | NotSetT | None): URI to fetch a JSON Web Key Set; used when verifying tokens with remote JWKS.
issuer (str | list[str] | NotSetT | None): Expected issuer claim value or list of allowed issuer values.
audience (str | list[str] | NotSetT | None): Expected audience claim value or list of allowed audience values.
algorithm (str | NotSetT | None): JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512.
required_scopes (list[str] | NotSetT | None): Scopes that must be present in validated tokens.
base_url (AnyHttpUrl | str | NotSetT | None): Base URL passed to the parent TokenVerifier.
public_key: PEM-encoded public key for asymmetric algorithms or shared secret for symmetric algorithms.
jwks_uri: URI to fetch a JSON Web Key Set; used when verifying tokens with remote JWKS.
issuer: Expected issuer claim value or list of allowed issuer values.
audience: Expected audience claim value or list of allowed audience values.
algorithm: JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512.
required_scopes: Scopes that must be present in validated tokens.
base_url: Base URL passed to the parent TokenVerifier.
Raises:
ValueError: If neither or both of `public_key` and `jwks_uri` are provided, or if `algorithm` is unsupported.
"""
settings = JWTVerifierSettings.model_validate(
{
k: v
for k, v in {
"public_key": public_key,
"jwks_uri": jwks_uri,
"issuer": issuer,
"audience": audience,
"algorithm": algorithm,
"required_scopes": required_scopes,
"base_url": base_url,
}.items()
if v is not NotSet
}
)
if not settings.public_key and not settings.jwks_uri:
if not public_key and not jwks_uri:
raise ValueError("Either public_key or jwks_uri must be provided")
if settings.public_key and settings.jwks_uri:
if public_key and jwks_uri:
raise ValueError("Provide either public_key or jwks_uri, not both")
algorithm = settings.algorithm or "RS256"
algorithm = algorithm or "RS256"
if algorithm not in {
"HS256",
"HS384",
@ -246,17 +204,22 @@ class JWTVerifier(TokenVerifier):
}:
raise ValueError(f"Unsupported algorithm: {algorithm}.")
# Parse scopes if provided as string
parsed_required_scopes = (
parse_scopes(required_scopes) if required_scopes is not None else None
)
# Initialize parent TokenVerifier
super().__init__(
base_url=settings.base_url,
required_scopes=settings.required_scopes,
base_url=base_url,
required_scopes=parsed_required_scopes,
)
self.algorithm = algorithm
self.issuer = settings.issuer
self.audience = settings.audience
self.public_key = settings.public_key
self.jwks_uri = settings.jwks_uri
self.issuer = issuer
self.audience = audience
self.public_key = public_key
self.jwks_uri = jwks_uri
self.jwt = JsonWebToken([self.algorithm])
self.logger = get_logger(__name__)

View file

@ -19,22 +19,22 @@ Example:
import os
# Load configuration from environment
FASTMCP_SERVER_AUTH_OCI_CONFIG_URL = os.environ["FASTMCP_SERVER_AUTH_OCI_CONFIG_URL"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_ID = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_ID"]
FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET"]
FASTMCP_SERVER_AUTH_OCI_IAM_GUID = os.environ["FASTMCP_SERVER_AUTH_OCI_IAM_GUID"]
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=FASTMCP_SERVER_AUTH_OCI_CONFIG_URL, #config URL is the OCI IAM Domain OIDC discovery URL.
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET, #This is same as the client secret configured for the OCI IAM Domain Integrated Application
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",
@ -42,7 +42,7 @@ Example:
# 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
_global_token_cache = {} # In memory cache for OCI session token signer
def get_oci_signer() -> TokenExchangeSigner:
@ -50,20 +50,20 @@ Example:
tokenID = authntoken.claims.get("jti")
token = authntoken.token
#Check if the signer exists for the token ID in memory cache
# 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
# 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=FASTMCP_SERVER_AUTH_OCI_IAM_GUID.split(".")[0], #This is same as IAM GUID configured for the OCI IAM Domain
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET #This is same as the client secret configured for the OCI IAM Domain Integrated Application
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}")
@ -78,44 +78,15 @@ Example:
"""
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class OCIProviderSettings(BaseSettings):
"""Settings for OCI IAM domain OIDC provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_OCI_",
env_file=ENV_FILE,
extra="ignore",
)
config_url: AnyHttpUrl | None = None
client_id: str | None = None
client_secret: SecretStr | None = None
audience: str | None = None
base_url: AnyHttpUrl | None = None
issuer_url: AnyHttpUrl | 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 | bytes | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class OCIProvider(OIDCProxy):
"""An OCI IAM Domain provider implementation for FastMCP.
@ -127,11 +98,13 @@ class OCIProvider(OIDCProxy):
from fastmcp import FastMCP
from fastmcp.server.auth.providers.oci import OCIProvider
# Simple OCI OIDC protection
import os
# Load configuration from environment
auth = OCIProvider(
config_url=FASTMCP_SERVER_AUTH_OCI_CONFIG_URL, #config URL is the OCI IAM Domain OIDC discovery URL.
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET, #This is same as the client secret configured for the OCI IAM Domain Integrated Application
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",
@ -144,17 +117,17 @@ class OCIProvider(OIDCProxy):
def __init__(
self,
*,
config_url: AnyHttpUrl | str | NotSetT = NotSet,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
audience: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
config_url: AnyHttpUrl | str,
client_id: str,
client_secret: str,
base_url: AnyHttpUrl | str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
) -> None:
"""Initialize OCI OIDC provider.
@ -163,71 +136,35 @@ class OCIProvider(OIDCProxy):
config_url: OCI OIDC Discovery URL
client_id: OCI IAM Domain Integrated Application client id
client_secret: OCI Integrated Application client secret
audience: OCI API audience (optional)
base_url: Public URL where OIDC endpoints will be accessible (includes any mount path)
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.
"""
overrides: dict[str, object] = {
k: v
for k, v in {
"config_url": config_url,
"client_id": client_id,
"client_secret": client_secret,
"audience": audience,
"base_url": base_url,
"issuer_url": issuer_url,
"required_scopes": required_scopes,
"redirect_path": redirect_path,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
}.items()
if v is not NotSet
}
settings = OCIProviderSettings(**overrides) # type: ignore[arg-type]
if not settings.config_url:
raise ValueError(
"config_url is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_CONFIG_URL"
)
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET"
)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_BASE_URL"
)
oci_required_scopes = settings.required_scopes or ["openid"]
# 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=settings.config_url,
client_id=settings.client_id,
client_secret=settings.client_secret.get_secret_value(),
audience=settings.audience,
base_url=settings.base_url,
issuer_url=settings.issuer_url,
redirect_path=settings.redirect_path,
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
audience=audience,
base_url=base_url,
issuer_url=issuer_url,
redirect_path=redirect_path,
required_scopes=oci_required_scopes,
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
allowed_client_redirect_uris=allowed_client_redirect_uris,
client_storage=client_storage,
jwt_signing_key=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
)
logger.debug(
"Initialized OCI OAuth provider for client %s with scopes: %s",
settings.client_id,
client_id,
oci_required_scopes,
)

View file

@ -8,50 +8,18 @@ authentication for seamless MCP client authentication.
from __future__ import annotations
import httpx
from pydantic import AnyHttpUrl, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class ScalekitProviderSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_",
env_file=ENV_FILE,
extra="ignore",
)
environment_url: AnyHttpUrl
resource_id: str
base_url: AnyHttpUrl | None = None
mcp_url: AnyHttpUrl | None = None
required_scopes: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, value: object):
return parse_scopes(value)
@model_validator(mode="after")
def _resolve_base_url(self):
resolved = self.base_url or self.mcp_url
if resolved is None:
msg = "Either base_url or mcp_url must be provided for ScalekitProvider"
raise ValueError(msg)
object.__setattr__(self, "base_url", resolved)
return self
class ScalekitProvider(RemoteAuthProvider):
"""Scalekit resource server provider for OAuth 2.1 authentication.
@ -95,12 +63,12 @@ class ScalekitProvider(RemoteAuthProvider):
def __init__(
self,
*,
environment_url: AnyHttpUrl | str | NotSetT = NotSet,
client_id: str | NotSetT = NotSet,
resource_id: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
mcp_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
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,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Scalekit resource server provider.
@ -108,42 +76,36 @@ class ScalekitProvider(RemoteAuthProvider):
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
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
token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit
"""
legacy_client_id = client_id is not NotSet
# 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")
settings = ScalekitProviderSettings.model_validate(
{
k: v
for k, v in {
"environment_url": environment_url,
"resource_id": resource_id,
"base_url": base_url,
"mcp_url": mcp_url,
"required_scopes": required_scopes,
}.items()
if v is not NotSet
}
)
if settings.mcp_url is not None:
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 legacy_client_id:
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(settings.environment_url).rstrip("/")
self.resource_id = settings.resource_id
self.required_scopes = settings.required_scopes or []
base_url_value = str(settings.base_url)
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",

View file

@ -10,39 +10,18 @@ from __future__ import annotations
from typing import Literal
import httpx
from pydantic import AnyHttpUrl, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class SupabaseProviderSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_SUPABASE_",
env_file=ENV_FILE,
extra="ignore",
)
project_url: AnyHttpUrl
base_url: AnyHttpUrl
algorithm: Literal["HS256", "RS256", "ES256"] = "ES256"
required_scopes: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class SupabaseProvider(RemoteAuthProvider):
"""Supabase metadata provider for DCR (Dynamic Client Registration).
@ -91,10 +70,10 @@ class SupabaseProvider(RemoteAuthProvider):
def __init__(
self,
*,
project_url: AnyHttpUrl | str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
algorithm: Literal["HS256", "RS256", "ES256"] | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
project_url: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
algorithm: Literal["HS256", "RS256", "ES256"] = "ES256",
required_scopes: list[str] | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Supabase metadata provider.
@ -109,29 +88,21 @@ class SupabaseProvider(RemoteAuthProvider):
scopes are an upcoming feature.
token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
"""
settings = SupabaseProviderSettings.model_validate(
{
k: v
for k, v in {
"project_url": project_url,
"base_url": base_url,
"algorithm": algorithm,
"required_scopes": required_scopes,
}.items()
if v is not NotSet
}
)
self.project_url = str(project_url).rstrip("/")
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
self.project_url = str(settings.project_url).rstrip("/")
self.base_url = AnyHttpUrl(str(settings.base_url).rstrip("/"))
# 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:
token_verifier = JWTVerifier(
jwks_uri=f"{self.project_url}/auth/v1/.well-known/jwks.json",
issuer=f"{self.project_url}/auth/v1",
algorithm=settings.algorithm,
required_scopes=settings.required_scopes,
algorithm=algorithm,
required_scopes=parsed_scopes,
)
# Initialize RemoteAuthProvider with Supabase as the authorization server

View file

@ -12,48 +12,19 @@ from __future__ import annotations
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl
from starlette.responses import JSONResponse
from starlette.routing import Route
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.settings import ENV_FILE
from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
logger = get_logger(__name__)
class WorkOSProviderSettings(BaseSettings):
"""Settings for WorkOS OAuth provider."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_WORKOS_",
env_file=ENV_FILE,
extra="ignore",
)
client_id: str | None = None
client_secret: SecretStr | None = None
authkit_domain: str | None = None # e.g., "https://your-app.authkit.app"
base_url: AnyHttpUrl | str | None = None
issuer_url: AnyHttpUrl | str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class WorkOSTokenVerifier(TokenVerifier):
"""Token verifier for WorkOS OAuth tokens.
@ -163,17 +134,17 @@ class WorkOSProvider(OAuthProxy):
def __init__(
self,
*,
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
authkit_domain: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
client_id: str,
client_secret: str,
authkit_domain: str,
base_url: AnyHttpUrl | str,
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 | NotSetT = NotSet,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
):
"""Initialize WorkOS OAuth provider.
@ -187,7 +158,7 @@ class WorkOSProvider(OAuthProxy):
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
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).
@ -201,106 +172,45 @@ class WorkOSProvider(OAuthProxy):
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
"""
settings = WorkOSProviderSettings.model_validate(
{
k: v
for k, v in {
"client_id": client_id,
"client_secret": client_secret,
"authkit_domain": authkit_domain,
"base_url": base_url,
"issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
}.items()
if v is not NotSet
}
)
# Validate required settings
if not settings.client_id:
raise ValueError(
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID"
)
if not settings.client_secret:
raise ValueError(
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET"
)
if not settings.authkit_domain:
raise ValueError(
"authkit_domain is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN"
)
if not settings.base_url:
raise ValueError(
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_BASE_URL"
)
# Apply defaults and ensure authkit_domain is a full URL
authkit_domain_str = settings.authkit_domain
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("/")
timeout_seconds_final = settings.timeout_seconds or 10
scopes_final = settings.required_scopes or []
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
# Extract secret string from SecretStr
client_secret_str = (
settings.client_secret.get_secret_value() if settings.client_secret else ""
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_final,
timeout_seconds=timeout_seconds,
)
# Initialize OAuth proxy with WorkOS AuthKit endpoints
super().__init__(
upstream_authorization_endpoint=f"{authkit_domain_final}/oauth2/authorize",
upstream_token_endpoint=f"{authkit_domain_final}/oauth2/token",
upstream_client_id=settings.client_id,
upstream_client_secret=client_secret_str,
upstream_client_id=client_id,
upstream_client_secret=client_secret,
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
issuer_url=settings.issuer_url
or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
base_url=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=settings.jwt_signing_key,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
)
logger.debug(
"Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
settings.client_id,
client_id,
authkit_domain_final,
)
class AuthKitProviderSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
env_file=ENV_FILE,
extra="ignore",
)
authkit_domain: AnyHttpUrl
base_url: AnyHttpUrl
required_scopes: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
def _parse_scopes(cls, v):
return parse_scopes(v)
class AuthKitProvider(RemoteAuthProvider):
"""AuthKit metadata provider for DCR (Dynamic Client Registration).
@ -340,9 +250,9 @@ class AuthKitProvider(RemoteAuthProvider):
def __init__(
self,
*,
authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
authkit_domain: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
required_scopes: list[str] | None = None,
token_verifier: TokenVerifier | None = None,
):
"""Initialize AuthKit metadata provider.
@ -353,20 +263,13 @@ class AuthKitProvider(RemoteAuthProvider):
required_scopes: Optional list of scopes to require for all requests
token_verifier: Optional token verifier. If None, creates JWT verifier for AuthKit
"""
settings = AuthKitProviderSettings.model_validate(
{
k: v
for k, v in {
"authkit_domain": authkit_domain,
"base_url": base_url,
"required_scopes": required_scopes,
}.items()
if v is not NotSet
}
)
self.authkit_domain = str(authkit_domain).rstrip("/")
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
self.authkit_domain = str(settings.authkit_domain).rstrip("/")
self.base_url = AnyHttpUrl(str(settings.base_url).rstrip("/"))
# 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:
@ -374,7 +277,7 @@ class AuthKitProvider(RemoteAuthProvider):
jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
issuer=self.authkit_domain,
algorithm="RS256",
required_scopes=settings.required_scopes,
required_scopes=parsed_scopes,
)
# Initialize RemoteAuthProvider with AuthKit as the authorization server

View file

@ -200,7 +200,7 @@ class FastMCP(Generic[LifespanResultT]):
version: str | None = None,
website_url: str | None = None,
icons: list[mcp.types.Icon] | None = None,
auth: AuthProvider | NotSetT | None = NotSet,
auth: AuthProvider | None = None,
middleware: Sequence[Middleware] | None = None,
providers: Sequence[Provider] | None = None,
lifespan: LifespanCallable | None = None,
@ -290,16 +290,6 @@ class FastMCP(Generic[LifespanResultT]):
lifespan=_lifespan_proxy(fastmcp_server=self),
)
# if auth is `NotSet`, try to create a provider from the environment
if auth is NotSet:
if fastmcp.settings.server_auth is not None:
# server_auth_class returns the class itself, not an instance
auth_class = cast(
type[AuthProvider], fastmcp.settings.server_auth_class
)
auth = auth_class()
else:
auth = None
self.auth: AuthProvider | None = auth
if tools:

View file

@ -5,10 +5,10 @@ import os
import warnings
from datetime import timedelta
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any, Literal
from typing import Annotated, Any, Literal
from platformdirs import user_data_dir
from pydantic import Field, ImportString, field_validator
from pydantic import Field, field_validator
from pydantic_settings import (
BaseSettings,
SettingsConfigDict,
@ -26,9 +26,6 @@ DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
TEN_MB_IN_BYTES = 1024 * 1024 * 10
if TYPE_CHECKING:
from fastmcp.server.auth.auth import AuthProvider
class DocketSettings(BaseSettings):
"""Docket worker configuration."""
@ -296,37 +293,6 @@ class Settings(BaseSettings):
False # If True, uses true stateless mode (new transport per request)
)
# Auth settings
server_auth: Annotated[
str | None,
Field(
description=inspect.cleandoc(
"""
Configure the authentication provider for the server by specifying
the full module path to an AuthProvider class (e.g.,
'fastmcp.server.auth.providers.google.GoogleProvider').
The specified class will be imported and instantiated automatically
during FastMCP server creation. Any class that inherits from AuthProvider
can be used, including custom implementations.
If None, no automatic configuration will take place.
This setting is *always* overridden by any auth provider passed to the
FastMCP constructor.
Note that most auth providers require additional configuration
that must be provided via env vars.
Examples:
- fastmcp.server.auth.providers.google.GoogleProvider
- fastmcp.server.auth.providers.jwt.JWTVerifier
- mycompany.auth.CustomAuthProvider
"""
),
),
] = None
include_fastmcp_meta: Annotated[
bool,
Field(
@ -366,22 +332,3 @@ class Settings(BaseSettings):
),
),
] = True
@property
def server_auth_class(self) -> AuthProvider | None:
from fastmcp.utilities.types import get_cached_typeadapter
if not self.server_auth:
return None
# https://github.com/jlowin/fastmcp/issues/1749
# Pydantic imports the module in an ImportString during model validation, but we don't want the server
# auth module imported during settings creation as it imports dependencies we aren't ready for yet.
# To fix this while limiting breaking changes, we delay the import by only creating the ImportString
# when the class is actually needed
type_adapter = get_cached_typeadapter(ImportString)
auth_class = type_adapter.validate_python(self.server_auth)
return auth_class

View file

@ -248,7 +248,6 @@ class TestDeprecatedServerInitKwargs:
"json_response": True,
"stateless_http": True,
}
mock_settings.server_auth = None # Add server_auth attribute
server = FastMCP("TestServer")
@ -278,7 +277,6 @@ class TestDeprecatedServerInitKwargs:
"json_response": True,
"stateless_http": True,
}
mock_settings.server_auth = None # Add server_auth attribute
with warnings.catch_warnings():
warnings.simplefilter("ignore") # Ignore warnings for this test

View file

@ -1,12 +1,11 @@
"""Unit tests for Auth0 OAuth provider."""
import os
from unittest.mock import patch
import pytest
from fastmcp.server.auth.oidc_proxy import OIDCConfiguration
from fastmcp.server.auth.providers.auth0 import Auth0Provider, Auth0ProviderSettings
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.auth.providers.jwt import JWTVerifier
TEST_CONFIG_URL = "https://example.com/.well-known/openid-configuration"
@ -32,62 +31,6 @@ def valid_oidc_configuration_dict():
}
class TestAuth0ProviderSettings:
"""Test settings for Auth0 OAuth provider."""
def test_settings_from_env_vars(self):
"""Test that settings can be loaded from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL": TEST_CONFIG_URL,
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID": TEST_CLIENT_ID,
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET": TEST_CLIENT_SECRET,
"FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE": TEST_AUDIENCE,
"FASTMCP_SERVER_AUTH_AUTH0_BASE_URL": TEST_BASE_URL,
"FASTMCP_SERVER_AUTH_AUTH0_REDIRECT_PATH": TEST_REDIRECT_PATH,
"FASTMCP_SERVER_AUTH_AUTH0_REQUIRED_SCOPES": ",".join(
TEST_REQUIRED_SCOPES
),
"FASTMCP_SERVER_AUTH_AUTH0_JWT_SIGNING_KEY": "test-secret",
},
):
settings = Auth0ProviderSettings()
assert str(settings.config_url) == TEST_CONFIG_URL
assert settings.client_id == TEST_CLIENT_ID
assert (
settings.client_secret
and settings.client_secret.get_secret_value() == TEST_CLIENT_SECRET
)
assert settings.audience == TEST_AUDIENCE
assert str(settings.base_url) == TEST_BASE_URL
assert settings.redirect_path == TEST_REDIRECT_PATH
assert settings.required_scopes == TEST_REQUIRED_SCOPES
def test_settings_explicit_override_env(self):
"""Test that explicit settings override environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID": TEST_CLIENT_ID,
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET": TEST_CLIENT_SECRET,
},
):
settings = Auth0ProviderSettings.model_validate(
{
"client_id": "explicit_client_id",
"client_secret": "explicit_secret",
}
)
assert settings.client_id == "explicit_client_id"
assert (
settings.client_secret
and settings.client_secret.get_secret_value() == "explicit_secret"
)
class TestAuth0Provider:
"""Test Auth0Provider initialization."""
@ -130,135 +73,6 @@ class TestAuth0Provider:
assert provider._redirect_path == TEST_REDIRECT_PATH
assert provider._token_validator.required_scopes == TEST_REQUIRED_SCOPES
@pytest.mark.parametrize(
"scopes_env",
[
"openid,email",
'["openid", "email"]',
],
)
def test_init_with_env_vars(self, scopes_env, valid_oidc_configuration_dict):
"""Test initialization with environment variables."""
with (
patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL": TEST_CONFIG_URL,
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID": TEST_CLIENT_ID,
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET": TEST_CLIENT_SECRET,
"FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE": TEST_AUDIENCE,
"FASTMCP_SERVER_AUTH_AUTH0_BASE_URL": TEST_BASE_URL,
"FASTMCP_SERVER_AUTH_AUTH0_REQUIRED_SCOPES": scopes_env,
"FASTMCP_SERVER_AUTH_AUTH0_JWT_SIGNING_KEY": "test-secret",
},
),
patch(
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration"
) as mock_get,
):
oidc_config = OIDCConfiguration.model_validate(
valid_oidc_configuration_dict
)
mock_get.return_value = oidc_config
provider = Auth0Provider()
mock_get.assert_called_once()
call_args = mock_get.call_args
assert str(call_args[0][0]) == TEST_CONFIG_URL
assert provider._upstream_client_id == TEST_CLIENT_ID
assert (
provider._upstream_client_secret.get_secret_value()
== TEST_CLIENT_SECRET
)
assert isinstance(provider._token_validator, JWTVerifier)
assert provider._token_validator.audience == TEST_AUDIENCE
assert str(provider.base_url) == TEST_BASE_URL
assert provider._token_validator.required_scopes == TEST_REQUIRED_SCOPES
def test_init_explicit_overrides_env(self, valid_oidc_configuration_dict):
"""Test that explicit parameters override environment variables."""
with (
patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL": TEST_CONFIG_URL,
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID": TEST_CLIENT_ID,
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET": TEST_CLIENT_SECRET,
"FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE": TEST_AUDIENCE,
"FASTMCP_SERVER_AUTH_AUTH0_BASE_URL": TEST_BASE_URL,
"FASTMCP_SERVER_AUTH_AUTH0_JWT_SIGNING_KEY": "test-secret",
},
),
patch(
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration"
) as mock_get,
):
oidc_config = OIDCConfiguration.model_validate(
valid_oidc_configuration_dict
)
mock_get.return_value = oidc_config
provider = Auth0Provider(
client_id="explicit_client",
client_secret="explicit_secret",
)
assert provider._upstream_client_id == "explicit_client"
assert (
provider._upstream_client_secret.get_secret_value() == "explicit_secret"
)
def test_init_missing_config_url_raises_error(self):
"""Test that missing config_url raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="config_url is required"):
Auth0Provider()
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_id is required"):
Auth0Provider(config_url=TEST_CONFIG_URL)
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_secret is required"):
Auth0Provider(config_url=TEST_CONFIG_URL, client_id=TEST_CLIENT_ID)
def test_init_missing_audience_raises_error(self):
"""Test that missing audience raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="audience is required"):
Auth0Provider(
config_url=TEST_CONFIG_URL,
client_id=TEST_CLIENT_ID,
client_secret=TEST_CLIENT_SECRET,
jwt_signing_key="test-secret",
)
def test_init_missing_base_url_raises_error(self):
"""Test that missing base_url raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="base_url is required"):
Auth0Provider(
config_url=TEST_CONFIG_URL,
client_id=TEST_CLIENT_ID,
client_secret=TEST_CLIENT_SECRET,
audience=TEST_AUDIENCE,
jwt_signing_key="test-secret",
)
def test_init_defaults(self, valid_oidc_configuration_dict):
"""Test that default values are applied correctly."""
with patch(

View file

@ -1,14 +1,10 @@
"""Unit tests for AWS Cognito OAuth provider."""
import os
from contextlib import contextmanager
from unittest.mock import patch
import pytest
from fastmcp.server.auth.providers.aws import (
AWSCognitoProvider,
AWSCognitoProviderSettings,
)
@ -38,60 +34,6 @@ def mock_cognito_oidc_discovery():
yield
class TestAWSCognitoProviderSettings:
"""Test settings for AWS Cognito OAuth provider."""
def test_settings_from_env_vars(self):
"""Test that settings can be loaded from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID": "us-east-1_XXXXXXXXX",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION": "us-east-1",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL": "https://example.com",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_REDIRECT_PATH": "/custom/callback",
},
):
settings = AWSCognitoProviderSettings()
assert settings.user_pool_id == "us-east-1_XXXXXXXXX"
assert settings.aws_region == "us-east-1"
assert settings.client_id == "env_client_id"
assert (
settings.client_secret
and settings.client_secret.get_secret_value() == "env_secret"
)
assert settings.base_url == "https://example.com"
assert settings.redirect_path == "/custom/callback"
def test_settings_explicit_override_env(self):
"""Test that explicit settings override environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID": "env_pool_id",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET": "env_secret",
},
):
settings = AWSCognitoProviderSettings.model_validate(
{
"user_pool_id": "explicit_pool_id",
"client_id": "explicit_client_id",
"client_secret": "explicit_secret",
}
)
assert settings.user_pool_id == "explicit_pool_id"
assert settings.client_id == "explicit_client_id"
assert (
settings.client_secret
and settings.client_secret.get_secret_value() == "explicit_secret"
)
class TestAWSCognitoProvider:
"""Test AWSCognitoProvider initialization."""
@ -126,92 +68,6 @@ class TestAWSCognitoProvider:
== "https://test.auth.us-east-1.amazoncognito.com/oauth2/token"
)
@pytest.mark.parametrize(
"scopes_env",
[
"openid,email",
'["openid", "email"]',
],
)
def test_init_with_env_vars(self, scopes_env):
"""Test initialization with environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID": "us-east-1_XXXXXXXXX",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION": "us-east-1",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL": "https://env-example.com",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_REQUIRED_SCOPES": scopes_env,
"FASTMCP_SERVER_AUTH_AWS_COGNITO_JWT_SIGNING_KEY": "test-secret",
},
):
with mock_cognito_oidc_discovery():
provider = AWSCognitoProvider()
assert provider._upstream_client_id == "env_client_id"
assert (
provider._upstream_client_secret.get_secret_value() == "env_secret"
)
assert str(provider.base_url) == "https://env-example.com/"
assert provider._token_validator.required_scopes == ["openid", "email"]
def test_init_explicit_overrides_env(self):
"""Test that explicit parameters override environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID": "env_pool_id",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_AWS_COGNITO_JWT_SIGNING_KEY": "test-secret",
},
):
with mock_cognito_oidc_discovery():
provider = AWSCognitoProvider(
user_pool_id="explicit_pool_id",
client_id="explicit_client",
client_secret="explicit_secret",
base_url="https://example.com",
jwt_signing_key="test-secret",
)
assert provider._upstream_client_id == "explicit_client"
assert (
provider._upstream_client_secret.get_secret_value()
== "explicit_secret"
)
# OIDC discovery should have configured the endpoints automatically
assert provider._upstream_authorization_endpoint is not None
def test_init_missing_user_pool_id_raises_error(self):
"""Test that missing user_pool_id raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="user_pool_id is required"):
AWSCognitoProvider(
client_id="test_client",
client_secret="test_secret",
)
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_id is required"):
AWSCognitoProvider(
user_pool_id="us-east-1_XXXXXXXXX",
client_secret="test_secret",
)
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_secret is required"):
AWSCognitoProvider(
user_pool_id="us-east-1_XXXXXXXXX",
client_id="test_client",
)
def test_init_defaults(self):
"""Test that default values are applied correctly."""
with mock_cognito_oidc_discovery():

View file

@ -1,10 +1,7 @@
"""Tests for Azure (Microsoft Entra) OAuth provider."""
import os
from unittest.mock import patch
from urllib.parse import parse_qs, urlparse
import pytest
from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyUrl
@ -36,115 +33,6 @@ class TestAzureProvider:
parsed_token = urlparse(provider._upstream_token_endpoint)
assert "87654321-4321-4321-4321-210987654321" in parsed_token.path
@pytest.mark.parametrize(
"scopes_env",
[
"read,write",
'["read", "write"]',
],
)
def test_init_with_env_vars(self, scopes_env):
"""Test AzureProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID": "env-client-id",
"FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET": "env-secret",
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID": "env-tenant-id",
"FASTMCP_SERVER_AUTH_AZURE_BASE_URL": "https://envserver.com",
"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES": scopes_env,
"FASTMCP_SERVER_AUTH_AZURE_JWT_SIGNING_KEY": "test-secret",
},
):
provider = AzureProvider()
assert provider._upstream_client_id == "env-client-id"
assert provider._upstream_client_secret.get_secret_value() == "env-secret"
assert str(provider.base_url) == "https://envserver.com/"
# Scopes are stored unprefixed for token validation
# (Azure returns unprefixed scopes in JWT tokens)
assert provider._token_validator.required_scopes == [
"read",
"write",
]
# Check tenant is in the endpoints
parsed_auth = urlparse(provider._upstream_authorization_endpoint)
assert "env-tenant-id" in parsed_auth.path
parsed_token = urlparse(provider._upstream_token_endpoint)
assert "env-tenant-id" in parsed_token.path
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
# Clear environment variables to ensure we're testing the parameter validation
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_id is required"):
AzureProvider(
client_secret="test_secret",
tenant_id="test-tenant",
required_scopes=["read"],
)
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
# Clear environment variables to ensure we're testing the parameter validation
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_secret is required"):
AzureProvider(
client_id="test_client",
tenant_id="test-tenant",
required_scopes=["read"],
)
def test_init_missing_tenant_id_raises_error(self):
"""Test that missing tenant_id raises ValueError."""
# Clear environment variables to ensure we're testing the parameter validation
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="tenant_id is required"):
AzureProvider(
client_id="test_client",
client_secret="test_secret",
required_scopes=["read"],
)
def test_init_missing_required_scopes_raises_error(self):
"""Test that missing required_scopes raises ValueError."""
# Clear environment variables to ensure we're testing the parameter validation
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(
ValueError, match="required_scopes must include at least one scope"
):
AzureProvider(
client_id="test_client",
client_secret="test_secret",
tenant_id="test-tenant",
)
def test_init_empty_required_scopes_raises_error(self):
"""Test that empty required_scopes raises ValueError."""
# Clear environment variables to ensure we're testing the parameter validation
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(
ValueError, match="required_scopes must include at least one scope"
):
AzureProvider(
client_id="test_client",
client_secret="test_secret",
tenant_id="test-tenant",
required_scopes=[],
)
def test_init_missing_base_url_raises_error(self):
"""Test that missing base_url raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="base_url is required"):
AzureProvider(
client_id="test_client",
client_secret="test_secret",
tenant_id="test-tenant",
required_scopes=["read"],
jwt_signing_key="test-secret",
)
def test_init_defaults(self):
"""Test that default values are applied correctly."""
provider = AzureProvider(
@ -463,39 +351,35 @@ class TestAzureProvider:
== "https://login.microsoftonline.us/gov-tenant-id/discovery/v2.0/keys"
)
def test_base_authority_from_environment_variable(self):
"""Test that base_authority can be set via environment variable."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID": "env-client-id",
"FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET": "env-secret",
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID": "env-tenant-id",
"FASTMCP_SERVER_AUTH_AZURE_BASE_URL": "https://myserver.com",
"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES": "read",
"FASTMCP_SERVER_AUTH_AZURE_BASE_AUTHORITY": "login.microsoftonline.us",
"FASTMCP_SERVER_AUTH_AZURE_JWT_SIGNING_KEY": "test-secret",
},
):
provider = AzureProvider()
def test_base_authority_from_parameter(self):
"""Test that base_authority can be set via parameter."""
provider = AzureProvider(
client_id="env-client-id",
client_secret="env-secret",
tenant_id="env-tenant-id",
base_url="https://myserver.com",
required_scopes=["read"],
base_authority="login.microsoftonline.us",
jwt_signing_key="test-secret",
)
assert (
provider._upstream_authorization_endpoint
== "https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/authorize"
)
assert (
provider._upstream_token_endpoint
== "https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/token"
)
assert isinstance(provider._token_validator, JWTVerifier)
assert (
provider._token_validator.issuer
== "https://login.microsoftonline.us/env-tenant-id/v2.0"
)
assert (
provider._token_validator.jwks_uri
== "https://login.microsoftonline.us/env-tenant-id/discovery/v2.0/keys"
)
assert (
provider._upstream_authorization_endpoint
== "https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/authorize"
)
assert (
provider._upstream_token_endpoint
== "https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/token"
)
assert isinstance(provider._token_validator, JWTVerifier)
assert (
provider._token_validator.issuer
== "https://login.microsoftonline.us/env-tenant-id/v2.0"
)
assert (
provider._token_validator.jwks_uri
== "https://login.microsoftonline.us/env-tenant-id/discovery/v2.0/keys"
)
def test_base_authority_with_special_tenant_values(self):
"""Test that base_authority works with special tenant values like 'organizations'."""

View file

@ -27,42 +27,6 @@ class TestDescopeProvider:
assert str(provider.base_url) == "https://myserver.com/"
assert str(provider.descope_base_url) == "https://api.descope.com"
def test_init_with_env_vars(self):
"""Test DescopeProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL": "https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration",
"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL": "https://envserver.com",
},
):
provider = DescopeProvider()
assert provider.project_id == "P2env123"
assert str(provider.base_url) == "https://envserver.com/"
assert str(provider.descope_base_url) == "https://api.descope.com"
def test_init_with_old_env_vars(self):
"""Test DescopeProvider initialization from old environment variables (backwards compatibility)."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_PROJECT_ID": "P2oldenv123",
"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_DESCOPE_BASE_URL": "https://api.descope.com",
"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL": "https://envserver.com",
},
):
provider = DescopeProvider()
assert provider.project_id == "P2oldenv123"
assert str(provider.base_url) == "https://envserver.com/"
assert str(provider.descope_base_url) == "https://api.descope.com"
assert isinstance(provider.token_verifier, JWTVerifier)
assert (
provider.token_verifier.issuer
== "https://api.descope.com/v1/apps/P2oldenv123"
)
def test_environment_variable_loading(self):
"""Test that environment variables are loaded correctly."""
# This test verifies that the provider can be created with environment variables
@ -219,7 +183,11 @@ class TestDescopeProvider:
"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_REQUIRED_SCOPES": "read,write",
},
):
provider = DescopeProvider()
provider = DescopeProvider(
config_url="https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration",
base_url="https://envserver.com",
required_scopes=["read", "write"],
)
assert isinstance(provider.token_verifier, JWTVerifier)
assert provider.token_verifier.required_scopes == ["read", "write"]

View file

@ -1,10 +1,5 @@
"""Tests for Discord OAuth provider."""
import os
from unittest.mock import patch
import pytest
from fastmcp.server.auth.providers.discord import DiscordProvider
@ -25,61 +20,6 @@ class TestDiscordProvider:
assert provider._upstream_client_secret.get_secret_value() == "GOCSPX-test123"
assert str(provider.base_url) == "https://myserver.com/"
@pytest.mark.parametrize(
"scopes_env",
[
"identify,email",
'["identify", "email"]',
],
)
def test_init_with_env_vars(self, scopes_env):
"""Test DiscordProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET": "GOCSPX-env456",
"FASTMCP_SERVER_AUTH_DISCORD_BASE_URL": "https://envserver.com",
"FASTMCP_SERVER_AUTH_DISCORD_REQUIRED_SCOPES": scopes_env,
"FASTMCP_SERVER_AUTH_DISCORD_JWT_SIGNING_KEY": "test-secret",
},
):
provider = DiscordProvider()
assert provider._upstream_client_id == "env_client_id"
assert (
provider._upstream_client_secret.get_secret_value() == "GOCSPX-env456"
)
assert str(provider.base_url) == "https://envserver.com/"
assert provider._token_validator.required_scopes == [
"identify",
"email",
]
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_id is required"):
DiscordProvider(client_secret="GOCSPX-test123")
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_secret is required"):
DiscordProvider(client_id="env_client_id")
def test_init_missing_base_url_raises_error(self):
"""Test that missing base_url raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="base_url is required"):
DiscordProvider(
client_id="env_client_id",
client_secret="GOCSPX-test123",
jwt_signing_key="test-secret",
)
def test_init_defaults(self):
"""Test that default values are applied correctly."""
provider = DiscordProvider(

View file

@ -1,68 +1,13 @@
"""Unit tests for GitHub OAuth provider."""
import os
from unittest.mock import MagicMock, patch
import pytest
from fastmcp.server.auth.providers.github import (
GitHubProvider,
GitHubProviderSettings,
GitHubTokenVerifier,
)
class TestGitHubProviderSettings:
"""Test settings for GitHub OAuth provider."""
def test_settings_from_env_vars(self):
"""Test that settings can be loaded from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_GITHUB_BASE_URL": "https://example.com",
"FASTMCP_SERVER_AUTH_GITHUB_REDIRECT_PATH": "/custom/callback",
"FASTMCP_SERVER_AUTH_GITHUB_TIMEOUT_SECONDS": "30",
"FASTMCP_SERVER_AUTH_GITHUB_JWT_SIGNING_KEY": "test-secret",
},
):
settings = GitHubProviderSettings()
assert settings.client_id == "env_client_id"
assert (
settings.client_secret
and settings.client_secret.get_secret_value() == "env_secret"
)
assert settings.base_url == "https://example.com"
assert settings.redirect_path == "/custom/callback"
assert settings.timeout_seconds == 30
def test_settings_explicit_override_env(self):
"""Test that explicit settings override environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET": "env_secret",
},
):
settings = GitHubProviderSettings.model_validate(
{
"client_id": "explicit_client_id",
"client_secret": "explicit_secret",
"jwt_signing_key": "test-secret",
}
)
assert settings.client_id == "explicit_client_id"
assert (
settings.client_secret
and settings.client_secret.get_secret_value() == "explicit_secret"
)
class TestGitHubProvider:
"""Test GitHubProvider initialization."""
@ -86,78 +31,6 @@ class TestGitHubProvider:
) # URLs get normalized with trailing slash
assert provider._redirect_path == "/custom/callback"
@pytest.mark.parametrize(
"scopes_env",
[
"user,repo",
'["user", "repo"]',
],
)
def test_init_with_env_vars(self, scopes_env):
"""Test initialization with environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_GITHUB_BASE_URL": "https://env-example.com",
"FASTMCP_SERVER_AUTH_GITHUB_REQUIRED_SCOPES": scopes_env,
"FASTMCP_SERVER_AUTH_GITHUB_JWT_SIGNING_KEY": "test-secret",
},
):
provider = GitHubProvider()
assert provider._upstream_client_id == "env_client_id"
assert provider._upstream_client_secret.get_secret_value() == "env_secret"
assert str(provider.base_url) == "https://env-example.com/"
assert provider._token_validator.required_scopes == ["user", "repo"]
def test_init_explicit_overrides_env(self):
"""Test that explicit parameters override environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_GITHUB_BASE_URL": "https://env-example.com",
"FASTMCP_SERVER_AUTH_GITHUB_JWT_SIGNING_KEY": "test-secret",
},
):
provider = GitHubProvider(
client_id="explicit_client",
client_secret="explicit_secret",
jwt_signing_key="test-secret",
)
assert provider._upstream_client_id == "explicit_client"
assert (
provider._upstream_client_secret.get_secret_value() == "explicit_secret"
)
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_id is required"):
GitHubProvider(client_secret="test_secret")
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_secret is required"):
GitHubProvider(client_id="test_client")
def test_init_missing_base_url_raises_error(self):
"""Test that missing base_url raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="base_url is required"):
GitHubProvider(
client_id="test_client",
client_secret="test_secret",
jwt_signing_key="test-secret",
)
def test_init_defaults(self):
"""Test that default values are applied correctly."""
provider = GitHubProvider(

View file

@ -1,10 +1,5 @@
"""Tests for Google OAuth provider."""
import os
from unittest.mock import patch
import pytest
from fastmcp.server.auth.providers.google import GoogleProvider
@ -25,61 +20,6 @@ class TestGoogleProvider:
assert provider._upstream_client_secret.get_secret_value() == "GOCSPX-test123"
assert str(provider.base_url) == "https://myserver.com/"
@pytest.mark.parametrize(
"scopes_env",
[
"openid,https://www.googleapis.com/auth/userinfo.email",
'["openid", "https://www.googleapis.com/auth/userinfo.email"]',
],
)
def test_init_with_env_vars(self, scopes_env):
"""Test GoogleProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID": "env123.apps.googleusercontent.com",
"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET": "GOCSPX-env456",
"FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL": "https://envserver.com",
"FASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES": scopes_env,
"FASTMCP_SERVER_AUTH_GOOGLE_JWT_SIGNING_KEY": "test-secret",
},
):
provider = GoogleProvider()
assert provider._upstream_client_id == "env123.apps.googleusercontent.com"
assert (
provider._upstream_client_secret.get_secret_value() == "GOCSPX-env456"
)
assert str(provider.base_url) == "https://envserver.com/"
assert provider._token_validator.required_scopes == [
"openid",
"https://www.googleapis.com/auth/userinfo.email",
]
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_id is required"):
GoogleProvider(client_secret="GOCSPX-test123")
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
# Clear environment variables to test proper error handling
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="client_secret is required"):
GoogleProvider(client_id="123456789.apps.googleusercontent.com")
def test_init_missing_base_url_raises_error(self):
"""Test that missing base_url raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="base_url is required"):
GoogleProvider(
client_id="123456789.apps.googleusercontent.com",
client_secret="GOCSPX-test123",
jwt_signing_key="test-secret",
)
def test_init_defaults(self):
"""Test that default values are applied correctly."""
provider = GoogleProvider(

View file

@ -5,57 +5,13 @@ import time
from typing import Any
import pytest
from pydantic import SecretStr
from pytest_httpx import HTTPXMock
from fastmcp.server.auth.providers.introspection import (
IntrospectionTokenVerifier,
IntrospectionTokenVerifierSettings,
)
class TestIntrospectionTokenVerifierSettings:
"""Test settings loading and validation."""
def test_settings_from_parameters(self):
"""Test creating settings from parameters."""
settings = IntrospectionTokenVerifierSettings(
introspection_url="https://auth.example.com/introspect",
client_id="test-client",
client_secret=SecretStr("test-secret"),
timeout_seconds=5,
required_scopes=["read", "write"],
)
assert settings.introspection_url == "https://auth.example.com/introspect"
assert settings.client_id == "test-client"
assert settings.client_secret
assert settings.client_secret.get_secret_value() == "test-secret"
assert settings.timeout_seconds == 5
assert settings.required_scopes == ["read", "write"]
def test_settings_default_timeout(self):
"""Test default timeout value."""
settings = IntrospectionTokenVerifierSettings(
introspection_url="https://auth.example.com/introspect",
client_id="test-client",
client_secret=SecretStr("test-secret"),
)
assert settings.timeout_seconds == 10
def test_settings_parse_scopes_from_string(self):
"""Test scope parsing from comma-separated string."""
settings = IntrospectionTokenVerifierSettings(
introspection_url="https://auth.example.com/introspect",
client_id="test-client",
client_secret=SecretStr("test-secret"),
required_scopes="read,write,admin", # type: ignore
)
assert settings.required_scopes == ["read", "write", "admin"]
class TestIntrospectionTokenVerifier:
"""Test core token verification logic."""
@ -94,24 +50,24 @@ class TestIntrospectionTokenVerifier:
def test_initialization_requires_introspection_url(self):
"""Test that introspection_url is required."""
with pytest.raises(ValueError, match="introspection_url is required"):
IntrospectionTokenVerifier(
with pytest.raises(TypeError):
IntrospectionTokenVerifier( # ty: ignore[missing-argument]
client_id="test-client",
client_secret="test-secret",
)
def test_initialization_requires_client_id(self):
"""Test that client_id is required."""
with pytest.raises(ValueError, match="client_id is required"):
IntrospectionTokenVerifier(
with pytest.raises(TypeError):
IntrospectionTokenVerifier( # ty: ignore[missing-argument]
introspection_url="https://auth.example.com/oauth/introspect",
client_secret="test-secret",
)
def test_initialization_requires_client_secret(self):
"""Test that client_secret is required."""
with pytest.raises(ValueError, match="client_secret is required"):
IntrospectionTokenVerifier(
with pytest.raises(TypeError):
IntrospectionTokenVerifier( # ty: ignore[missing-argument]
introspection_url="https://auth.example.com/oauth/introspect",
client_id="test-client",
)

View file

@ -1,8 +1,5 @@
"""Tests for Scalekit OAuth provider."""
import os
from unittest.mock import patch
import httpx
import pytest
@ -51,38 +48,6 @@ class TestScalekitProvider:
assert str(provider.base_url) == "https://preferred-base.com/"
def test_init_with_env_vars(self):
"""Test ScalekitProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL": "https://env-scalekit.com",
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID": "res_456",
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_BASE_URL": "https://envserver.com/mcp",
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_REQUIRED_SCOPES": "read,write",
},
):
provider = ScalekitProvider()
assert provider.environment_url == "https://env-scalekit.com"
assert provider.resource_id == "res_456"
assert str(provider.base_url) == "https://envserver.com/mcp"
assert provider.required_scopes == ["read", "write"]
def test_init_with_legacy_env_var(self):
"""FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL should still be supported."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL": "https://env-scalekit.com",
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID": "res_456",
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL": "https://legacy-env.com/",
},
):
provider = ScalekitProvider()
assert str(provider.base_url) == "https://legacy-env.com/"
def test_environment_variable_loading(self):
"""Test that environment variables are loaded correctly."""
provider = ScalekitProvider(

View file

@ -1,8 +1,6 @@
"""Tests for Supabase Auth provider."""
import os
from collections.abc import Generator
from unittest.mock import patch
import httpx
import pytest
@ -27,27 +25,6 @@ class TestSupabaseProvider:
assert provider.project_url == "https://abc123.supabase.co"
assert str(provider.base_url) == "https://myserver.com/"
@pytest.mark.parametrize(
"scopes_env",
[
"openid,email",
'["openid", "email"]',
],
)
def test_init_with_env_vars(self, scopes_env):
"""Test SupabaseProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL": "https://env123.supabase.co",
"FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL": "https://envserver.com",
},
):
provider = SupabaseProvider()
assert provider.project_url == "https://env123.supabase.co"
assert str(provider.base_url) == "https://envserver.com/"
def test_environment_variable_loading(self):
"""Test that environment variables are loaded correctly."""
provider = SupabaseProvider(
@ -139,20 +116,16 @@ class TestSupabaseProvider:
assert isinstance(provider.token_verifier, JWTVerifier)
assert provider.token_verifier.algorithm == "ES256"
def test_algorithm_from_env_var(self):
"""Test that algorithm can be configured via environment variable."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL": "https://env123.supabase.co",
"FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL": "https://envserver.com",
"FASTMCP_SERVER_AUTH_SUPABASE_ALGORITHM": "RS256",
},
):
provider = SupabaseProvider()
def test_algorithm_from_parameter(self):
"""Test that algorithm can be configured via parameter."""
provider = SupabaseProvider(
project_url="https://env123.supabase.co",
base_url="https://envserver.com",
algorithm="RS256",
)
assert isinstance(provider.token_verifier, JWTVerifier)
assert provider.token_verifier.algorithm == "RS256"
assert isinstance(provider.token_verifier, JWTVerifier)
assert provider.token_verifier.algorithm == "RS256"
def run_mcp_server(host: str, port: int) -> None:

View file

@ -1,7 +1,5 @@
"""Tests for WorkOS OAuth provider."""
import os
from unittest.mock import patch
from urllib.parse import urlparse
import httpx
@ -31,60 +29,6 @@ class TestWorkOSProvider:
assert provider._upstream_client_secret.get_secret_value() == "secret_test456"
assert str(provider.base_url) == "https://myserver.com/"
@pytest.mark.parametrize(
"scopes_env",
[
"openid,email",
'["openid", "email"]',
],
)
def test_init_with_env_vars(self, scopes_env):
"""Test WorkOSProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
"FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID": "env_client",
"FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN": "https://env.authkit.app",
"FASTMCP_SERVER_AUTH_WORKOS_BASE_URL": "https://envserver.com",
"FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES": scopes_env,
"FASTMCP_SERVER_AUTH_WORKOS_JWT_SIGNING_KEY": "test-secret",
},
):
provider = WorkOSProvider()
assert provider._upstream_client_id == "env_client"
assert provider._upstream_client_secret.get_secret_value() == "env_secret"
assert str(provider.base_url) == "https://envserver.com/"
assert provider._token_validator.required_scopes == [
"openid",
"email",
]
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
with pytest.raises(ValueError, match="client_id is required"):
WorkOSProvider(
client_secret="test_secret",
authkit_domain="https://test.authkit.app",
)
def test_init_missing_client_secret_raises_error(self):
"""Test that missing client_secret raises ValueError."""
with pytest.raises(ValueError, match="client_secret is required"):
WorkOSProvider(
client_id="test_client",
authkit_domain="https://test.authkit.app",
)
def test_init_missing_authkit_domain_raises_error(self):
"""Test that missing authkit_domain raises ValueError."""
with pytest.raises(ValueError, match="authkit_domain is required"):
WorkOSProvider(
client_id="test_client",
client_secret="test_secret",
)
def test_authkit_domain_https_prefix_handling(self):
"""Test that authkit_domain handles missing https:// prefix."""
# Without https:// - should add it
@ -126,17 +70,6 @@ class TestWorkOSProvider:
assert parsed.netloc == "localhost:8080"
assert parsed.path == "/oauth2/authorize"
def test_init_missing_base_url_raises_error(self):
"""Test that missing base_url raises ValueError."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="base_url is required"):
WorkOSProvider(
client_id="test_client",
client_secret="test_secret",
authkit_domain="https://test.authkit.app",
jwt_signing_key="test-secret",
)
def test_init_defaults(self):
"""Test that default values are applied correctly."""
provider = WorkOSProvider(

View file

@ -169,25 +169,11 @@ class TestResourcePrefixMounting:
class TestSettingsFromEnvironment:
async def test_settings_from_environment_issue_1749(self):
"""Test that when auth is enabled, the server starts."""
async def test_server_starts_without_auth(self):
"""Test that server starts without auth configured."""
from fastmcp.client.transports import PythonStdioTransport
from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.settings import Settings
script = dedent("""
import os
os.environ["FASTMCP_SERVER_AUTH"] = "fastmcp.server.auth.providers.azure.AzureProvider"
os.environ["FASTMCP_SERVER_AUTH_AZURE_TENANT_ID"] = "A_Valid_Value"
os.environ["FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"] = "A_Valid_Value"
os.environ["FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"] = "A_Valid_Value"
os.environ["FASTMCP_SERVER_AUTH_AZURE_REDIRECT_PATH"] = "/auth/callback"
os.environ["FASTMCP_SERVER_AUTH_AZURE_BASE_URL"] = "http://localhost:8000"
os.environ["FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES"] = "User.Read,email,profile"
os.environ["FASTMCP_SERVER_AUTH_AZURE_JWT_SIGNING_KEY"] = "test-secret"
import fastmcp
mcp = fastmcp.FastMCP("TestServer")
@ -208,14 +194,6 @@ class TestSettingsFromEnvironment:
assert tools == []
settings = Settings(
server_auth="fastmcp.server.auth.providers.azure.AzureProvider"
)
auth_class = settings.server_auth_class
assert auth_class is AzureProvider
class TestAbstractCollectionTypes:
"""Test that FastMCP accepts abstract collection types from collections.abc."""