mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 21:14:17 +02:00
Rename OAuth providers to include DCR suffix
Renames all OAuth providers that inherit from OAuthDCRProxy to explicitly include "DCR" in their names, clarifying their Dynamic Client Registration implementation approach. Changes: - GitHubProvider → GitHubDCRProvider - GoogleProvider → GoogleDCRProvider - AzureProvider → AzureDCRProvider - WorkOSProvider → WorkOSDCRProvider - Auth0Provider → Auth0DCRProvider - AWSCognitoProvider → AWSCognitoDCRProvider All old names remain as deprecated aliases with warnings that respect settings.deprecation_warnings. Environment variables updated to include _DCR_ with backwards compatibility via env_prefixes.
This commit is contained in:
parent
deb0c3ea95
commit
0ad638003c
13 changed files with 676 additions and 342 deletions
|
|
@ -6,10 +6,10 @@ just the configuration URL, client ID, client secret, audience, and base URL.
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0Provider
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0DCRProvider
|
||||
|
||||
# Simple Auth0 OAuth protection
|
||||
auth = Auth0Provider(
|
||||
auth = Auth0DCRProvider(
|
||||
config_url="https://auth0.config.url",
|
||||
client_id="your-auth0-client-id",
|
||||
client_secret="your-auth0-client-secret",
|
||||
|
|
@ -21,12 +21,19 @@ Example:
|
|||
```
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from fastmcp.server.auth.oidc_dcr_proxy import OIDCDCRProxy
|
||||
from fastmcp.settings import ENV_FILE
|
||||
from fastmcp.settings import (
|
||||
ENV_FILE,
|
||||
ExtendedEnvSettingsSource,
|
||||
ExtendedSettingsConfigDict,
|
||||
settings,
|
||||
)
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
|
@ -34,15 +41,32 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Auth0ProviderSettings(BaseSettings):
|
||||
"""Settings for Auth0 OIDC provider."""
|
||||
class Auth0DCRProviderSettings(BaseSettings):
|
||||
"""Settings for Auth0 OIDC DCR provider."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_AUTH0_",
|
||||
model_config = ExtendedSettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_AUTH0_DCR_",
|
||||
env_prefixes=["FASTMCP_SERVER_AUTH_AUTH0_DCR_", "FASTMCP_SERVER_AUTH_AUTH0_"],
|
||||
env_file=ENV_FILE,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls,
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
):
|
||||
return (
|
||||
init_settings,
|
||||
ExtendedEnvSettingsSource(settings_cls),
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
config_url: AnyHttpUrl | None = None
|
||||
client_id: str | None = None
|
||||
client_secret: SecretStr | None = None
|
||||
|
|
@ -59,8 +83,8 @@ class Auth0ProviderSettings(BaseSettings):
|
|||
return parse_scopes(v)
|
||||
|
||||
|
||||
class Auth0Provider(OIDCDCRProxy):
|
||||
"""An Auth0 provider implementation for FastMCP.
|
||||
class Auth0DCRProvider(OIDCDCRProxy):
|
||||
"""An Auth0 DCR provider implementation for FastMCP.
|
||||
|
||||
This provider is a complete Auth0 integration that's ready to use with
|
||||
just the configuration URL, client ID, client secret, audience, and base URL.
|
||||
|
|
@ -68,10 +92,10 @@ class Auth0Provider(OIDCDCRProxy):
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0Provider
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0DCRProvider
|
||||
|
||||
# Simple Auth0 OAuth protection
|
||||
auth = Auth0Provider(
|
||||
auth = Auth0DCRProvider(
|
||||
config_url="https://auth0.config.url",
|
||||
client_id="your-auth0-client-id",
|
||||
client_secret="your-auth0-client-secret",
|
||||
|
|
@ -113,7 +137,7 @@ class Auth0Provider(OIDCDCRProxy):
|
|||
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
||||
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
||||
"""
|
||||
settings = Auth0ProviderSettings.model_validate(
|
||||
provider_settings = Auth0DCRProviderSettings.model_validate(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
|
|
@ -131,50 +155,67 @@ class Auth0Provider(OIDCDCRProxy):
|
|||
}
|
||||
)
|
||||
|
||||
if not settings.config_url:
|
||||
if not provider_settings.config_url:
|
||||
raise ValueError(
|
||||
"config_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL"
|
||||
"config_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_DCR_CONFIG_URL"
|
||||
)
|
||||
|
||||
if not settings.client_id:
|
||||
if not provider_settings.client_id:
|
||||
raise ValueError(
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID"
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_ID"
|
||||
)
|
||||
|
||||
if not settings.client_secret:
|
||||
if not provider_settings.client_secret:
|
||||
raise ValueError(
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET"
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_SECRET"
|
||||
)
|
||||
|
||||
if not settings.audience:
|
||||
if not provider_settings.audience:
|
||||
raise ValueError(
|
||||
"audience is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE"
|
||||
"audience is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_DCR_AUDIENCE"
|
||||
)
|
||||
|
||||
if not settings.base_url:
|
||||
if not provider_settings.base_url:
|
||||
raise ValueError(
|
||||
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_BASE_URL"
|
||||
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_DCR_BASE_URL"
|
||||
)
|
||||
|
||||
auth0_required_scopes = settings.required_scopes or ["openid"]
|
||||
auth0_required_scopes = provider_settings.required_scopes or ["openid"]
|
||||
|
||||
init_kwargs = {
|
||||
"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": provider_settings.config_url,
|
||||
"client_id": provider_settings.client_id,
|
||||
"client_secret": provider_settings.client_secret.get_secret_value(),
|
||||
"audience": provider_settings.audience,
|
||||
"base_url": provider_settings.base_url,
|
||||
"issuer_url": provider_settings.issuer_url,
|
||||
"redirect_path": provider_settings.redirect_path,
|
||||
"required_scopes": auth0_required_scopes,
|
||||
"allowed_client_redirect_uris": settings.allowed_client_redirect_uris,
|
||||
"allowed_client_redirect_uris": provider_settings.allowed_client_redirect_uris,
|
||||
"client_storage": client_storage,
|
||||
}
|
||||
|
||||
super().__init__(**init_kwargs)
|
||||
|
||||
logger.info(
|
||||
"Initialized Auth0 OAuth provider for client %s with scopes: %s",
|
||||
settings.client_id,
|
||||
"Initialized Auth0 OAuth DCR provider for client %s with scopes: %s",
|
||||
provider_settings.client_id,
|
||||
auth0_required_scopes,
|
||||
)
|
||||
|
||||
|
||||
# Deprecated alias for backwards compatibility
|
||||
class Auth0Provider(Auth0DCRProvider):
|
||||
"""Deprecated: Use Auth0DCRProvider instead.
|
||||
|
||||
This alias is provided for backwards compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"Auth0Provider is deprecated, use Auth0DCRProvider instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -23,15 +23,22 @@ Example:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from fastmcp.server.auth import TokenVerifier
|
||||
from fastmcp.server.auth.auth import AccessToken
|
||||
from fastmcp.server.auth.oidc_dcr_proxy import OIDCDCRProxy
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
from fastmcp.settings import ENV_FILE
|
||||
from fastmcp.settings import (
|
||||
ENV_FILE,
|
||||
ExtendedEnvSettingsSource,
|
||||
ExtendedSettingsConfigDict,
|
||||
settings,
|
||||
)
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
|
@ -39,15 +46,35 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AWSCognitoProviderSettings(BaseSettings):
|
||||
"""Settings for AWS Cognito OAuth provider."""
|
||||
class AWSCognitoDCRProviderSettings(BaseSettings):
|
||||
"""Settings for AWS Cognito OAuth DCR provider."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_AWS_COGNITO_",
|
||||
model_config = ExtendedSettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_",
|
||||
env_prefixes=[
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_",
|
||||
],
|
||||
env_file=ENV_FILE,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls,
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
):
|
||||
return (
|
||||
init_settings,
|
||||
ExtendedEnvSettingsSource(settings_cls),
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
user_pool_id: str | None = None
|
||||
aws_region: str | None = None
|
||||
client_id: str | None = None
|
||||
|
|
@ -91,8 +118,8 @@ class AWSCognitoTokenVerifier(JWTVerifier):
|
|||
)
|
||||
|
||||
|
||||
class AWSCognitoProvider(OIDCDCRProxy):
|
||||
"""Complete AWS Cognito OAuth provider for FastMCP.
|
||||
class AWSCognitoDCRProvider(OIDCDCRProxy):
|
||||
"""Complete AWS Cognito OAuth DCR provider for FastMCP.
|
||||
|
||||
This provider makes it trivial to add AWS Cognito OAuth protection to any
|
||||
FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,
|
||||
|
|
@ -107,9 +134,9 @@ class AWSCognitoProvider(OIDCDCRProxy):
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
|
||||
from fastmcp.server.auth.providers.aws_cognito import AWSCognitoDCRProvider
|
||||
|
||||
auth = AWSCognitoProvider(
|
||||
auth = AWSCognitoDCRProvider(
|
||||
user_pool_id="eu-central-1_XXXXXXXXX",
|
||||
aws_region="eu-central-1",
|
||||
client_id="your-cognito-client-id",
|
||||
|
|
@ -153,7 +180,7 @@ class AWSCognitoProvider(OIDCDCRProxy):
|
|||
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
||||
"""
|
||||
|
||||
settings = AWSCognitoProviderSettings.model_validate(
|
||||
provider_settings = AWSCognitoDCRProviderSettings.model_validate(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
|
|
@ -172,57 +199,78 @@ class AWSCognitoProvider(OIDCDCRProxy):
|
|||
)
|
||||
|
||||
# Validate required settings
|
||||
if not settings.user_pool_id:
|
||||
if not provider_settings.user_pool_id:
|
||||
raise ValueError(
|
||||
"user_pool_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID"
|
||||
"user_pool_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_USER_POOL_ID"
|
||||
)
|
||||
if not settings.client_id:
|
||||
if not provider_settings.client_id:
|
||||
raise ValueError(
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID"
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_ID"
|
||||
)
|
||||
if not settings.client_secret:
|
||||
if not provider_settings.client_secret:
|
||||
raise ValueError(
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET"
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_SECRET"
|
||||
)
|
||||
|
||||
# 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"
|
||||
required_scopes_final = provider_settings.required_scopes or ["openid"]
|
||||
allowed_client_redirect_uris_final = (
|
||||
provider_settings.allowed_client_redirect_uris
|
||||
)
|
||||
aws_region_final = provider_settings.aws_region or "eu-central-1"
|
||||
redirect_path_final = provider_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"
|
||||
config_url = f"https://cognito-idp.{aws_region_final}.amazonaws.com/{provider_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 ""
|
||||
provider_settings.client_secret.get_secret_value()
|
||||
if provider_settings.client_secret
|
||||
else ""
|
||||
)
|
||||
|
||||
# Store Cognito-specific info for claim filtering
|
||||
self.user_pool_id = settings.user_pool_id
|
||||
self.user_pool_id = provider_settings.user_pool_id
|
||||
self.aws_region = aws_region_final
|
||||
|
||||
# Initialize OIDC proxy with Cognito discovery
|
||||
super().__init__(
|
||||
config_url=config_url,
|
||||
client_id=settings.client_id,
|
||||
client_id=provider_settings.client_id,
|
||||
client_secret=client_secret_str,
|
||||
algorithm="RS256",
|
||||
required_scopes=required_scopes_final,
|
||||
base_url=settings.base_url,
|
||||
issuer_url=settings.issuer_url,
|
||||
base_url=provider_settings.base_url,
|
||||
issuer_url=provider_settings.issuer_url,
|
||||
redirect_path=redirect_path_final,
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
client_storage=client_storage,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
|
||||
settings.client_id,
|
||||
"Initialized AWS Cognito OAuth DCR provider for client %s with scopes: %s",
|
||||
provider_settings.client_id,
|
||||
required_scopes_final,
|
||||
)
|
||||
|
||||
|
||||
# Deprecated alias for backwards compatibility
|
||||
class AWSCognitoProvider(AWSCognitoDCRProvider):
|
||||
"""Deprecated: Use AWSCognitoDCRProvider instead.
|
||||
|
||||
This alias is provided for backwards compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"AWSCognitoProvider is deprecated, use AWSCognitoDCRProvider instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_token_verifier(
|
||||
self,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -6,15 +6,21 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
from pydantic import SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from fastmcp.server.auth.oauth_dcr_proxy import OAuthDCRProxy
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
from fastmcp.settings import ENV_FILE
|
||||
from fastmcp.settings import (
|
||||
ENV_FILE,
|
||||
ExtendedEnvSettingsSource,
|
||||
ExtendedSettingsConfigDict,
|
||||
settings,
|
||||
)
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
|
@ -26,15 +32,32 @@ if TYPE_CHECKING:
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AzureProviderSettings(BaseSettings):
|
||||
"""Settings for Azure OAuth provider."""
|
||||
class AzureDCRProviderSettings(BaseSettings):
|
||||
"""Settings for Azure OAuth DCR provider."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
|
||||
model_config = ExtendedSettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_AZURE_DCR_",
|
||||
env_prefixes=["FASTMCP_SERVER_AUTH_AZURE_DCR_", "FASTMCP_SERVER_AUTH_AZURE_"],
|
||||
env_file=ENV_FILE,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls,
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
):
|
||||
return (
|
||||
init_settings,
|
||||
ExtendedEnvSettingsSource(settings_cls),
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
client_id: str | None = None
|
||||
client_secret: SecretStr | None = None
|
||||
tenant_id: str | None = None
|
||||
|
|
@ -57,8 +80,8 @@ class AzureProviderSettings(BaseSettings):
|
|||
return parse_scopes(v)
|
||||
|
||||
|
||||
class AzureProvider(OAuthDCRProxy):
|
||||
"""Azure (Microsoft Entra) OAuth provider for FastMCP.
|
||||
class AzureDCRProvider(OAuthDCRProxy):
|
||||
"""Azure (Microsoft Entra) OAuth DCR provider for FastMCP.
|
||||
|
||||
This provider implements Azure/Microsoft Entra ID authentication using the
|
||||
OAuth Proxy pattern. It supports both organizational accounts and personal
|
||||
|
|
@ -80,9 +103,9 @@ class AzureProvider(OAuthDCRProxy):
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider
|
||||
from fastmcp.server.auth.providers.azure import AzureDCRProvider
|
||||
|
||||
auth = AzureProvider(
|
||||
auth = AzureDCRProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
tenant_id="your-tenant-id",
|
||||
|
|
@ -132,7 +155,7 @@ class AzureProvider(OAuthDCRProxy):
|
|||
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
||||
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
||||
"""
|
||||
settings = AzureProviderSettings.model_validate(
|
||||
provider_settings = AzureDCRProviderSettings.model_validate(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
|
|
@ -152,29 +175,33 @@ class AzureProvider(OAuthDCRProxy):
|
|||
)
|
||||
|
||||
# Validate required settings
|
||||
if not settings.client_id:
|
||||
msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
|
||||
if not provider_settings.client_id:
|
||||
msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_DCR_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"
|
||||
if not provider_settings.client_secret:
|
||||
msg = "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_DCR_CLIENT_SECRET"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Validate tenant_id is provided
|
||||
if not settings.tenant_id:
|
||||
if not provider_settings.tenant_id:
|
||||
msg = (
|
||||
"tenant_id is required - set via parameter or "
|
||||
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. Use your Azure tenant ID "
|
||||
"FASTMCP_SERVER_AUTH_AZURE_DCR_TENANT_ID. Use your Azure tenant ID "
|
||||
"(found in Azure Portal), 'organizations', or 'consumers'"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
if not settings.required_scopes:
|
||||
if not provider_settings.required_scopes:
|
||||
raise ValueError("required_scopes is required")
|
||||
|
||||
# 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 = (
|
||||
provider_settings.identifier_uri or f"api://{provider_settings.client_id}"
|
||||
)
|
||||
self.additional_authorize_scopes = (
|
||||
provider_settings.additional_authorize_scopes or []
|
||||
)
|
||||
tenant_id_final = provider_settings.tenant_id
|
||||
|
||||
# Always validate tokens against the app's API client ID using JWT
|
||||
issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
|
||||
|
|
@ -185,14 +212,16 @@ class AzureProvider(OAuthDCRProxy):
|
|||
token_verifier = JWTVerifier(
|
||||
jwks_uri=jwks_uri,
|
||||
issuer=issuer,
|
||||
audience=settings.client_id,
|
||||
audience=provider_settings.client_id,
|
||||
algorithm="RS256",
|
||||
required_scopes=settings.required_scopes,
|
||||
required_scopes=provider_settings.required_scopes,
|
||||
)
|
||||
|
||||
# Extract secret string from SecretStr
|
||||
client_secret_str = (
|
||||
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
||||
provider_settings.client_secret.get_secret_value()
|
||||
if provider_settings.client_secret
|
||||
else ""
|
||||
)
|
||||
|
||||
# Build Azure OAuth endpoints with tenant
|
||||
|
|
@ -207,24 +236,41 @@ class AzureProvider(OAuthDCRProxy):
|
|||
super().__init__(
|
||||
upstream_authorization_endpoint=authorization_endpoint,
|
||||
upstream_token_endpoint=token_endpoint,
|
||||
upstream_client_id=settings.client_id,
|
||||
upstream_client_id=provider_settings.client_id,
|
||||
upstream_client_secret=client_secret_str,
|
||||
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=provider_settings.base_url,
|
||||
redirect_path=provider_settings.redirect_path,
|
||||
issuer_url=provider_settings.issuer_url
|
||||
or provider_settings.base_url, # Default to base_url if not specified
|
||||
allowed_client_redirect_uris=provider_settings.allowed_client_redirect_uris,
|
||||
client_storage=client_storage,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Initialized Azure OAuth provider for client %s with tenant %s%s",
|
||||
settings.client_id,
|
||||
"Initialized Azure OAuth DCR provider for client %s with tenant %s%s",
|
||||
provider_settings.client_id,
|
||||
tenant_id_final,
|
||||
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
|
||||
)
|
||||
|
||||
|
||||
# Deprecated alias for backwards compatibility
|
||||
class AzureProvider(AzureDCRProvider):
|
||||
"""Deprecated: Use AzureDCRProvider instead.
|
||||
|
||||
This alias is provided for backwards compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"AzureProvider is deprecated, use AzureDCRProvider instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
async def authorize(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ GitHub's OAuth flow, token validation, and user management.
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.github import GitHubProvider
|
||||
from fastmcp.server.auth.providers.github import GitHubDCRProvider
|
||||
|
||||
# Simple GitHub OAuth protection
|
||||
auth = GitHubProvider(
|
||||
auth = GitHubDCRProvider(
|
||||
client_id="your-github-client-id",
|
||||
client_secret="your-github-client-secret"
|
||||
)
|
||||
|
|
@ -21,15 +21,22 @@ Example:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import httpx
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from fastmcp.server.auth import TokenVerifier
|
||||
from fastmcp.server.auth.auth import AccessToken
|
||||
from fastmcp.server.auth.oauth_dcr_proxy import OAuthDCRProxy
|
||||
from fastmcp.settings import ENV_FILE
|
||||
from fastmcp.settings import (
|
||||
ENV_FILE,
|
||||
ExtendedEnvSettingsSource,
|
||||
ExtendedSettingsConfigDict,
|
||||
settings,
|
||||
)
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
|
@ -37,15 +44,32 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class GitHubProviderSettings(BaseSettings):
|
||||
"""Settings for GitHub OAuth provider."""
|
||||
class GitHubDCRProviderSettings(BaseSettings):
|
||||
"""Settings for GitHub OAuth DCR provider."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_GITHUB_",
|
||||
model_config = ExtendedSettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_GITHUB_DCR_",
|
||||
env_prefixes=["FASTMCP_SERVER_AUTH_GITHUB_DCR_", "FASTMCP_SERVER_AUTH_GITHUB_"],
|
||||
env_file=ENV_FILE,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls,
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
):
|
||||
return (
|
||||
init_settings,
|
||||
ExtendedEnvSettingsSource(settings_cls),
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
client_id: str | None = None
|
||||
client_secret: SecretStr | None = None
|
||||
base_url: AnyHttpUrl | str | None = None
|
||||
|
|
@ -166,8 +190,8 @@ class GitHubTokenVerifier(TokenVerifier):
|
|||
return None
|
||||
|
||||
|
||||
class GitHubProvider(OAuthDCRProxy):
|
||||
"""Complete GitHub OAuth provider for FastMCP.
|
||||
class GitHubDCRProvider(OAuthDCRProxy):
|
||||
"""Complete GitHub OAuth DCR provider for FastMCP.
|
||||
|
||||
This provider makes it trivial to add GitHub OAuth protection to any
|
||||
FastMCP server. Just provide your GitHub OAuth app credentials and
|
||||
|
|
@ -182,9 +206,9 @@ class GitHubProvider(OAuthDCRProxy):
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.github import GitHubProvider
|
||||
from fastmcp.server.auth.providers.github import GitHubDCRProvider
|
||||
|
||||
auth = GitHubProvider(
|
||||
auth = GitHubDCRProvider(
|
||||
client_id="Ov23li...",
|
||||
client_secret="abc123...",
|
||||
base_url="https://my-server.com"
|
||||
|
|
@ -223,7 +247,7 @@ class GitHubProvider(OAuthDCRProxy):
|
|||
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
||||
"""
|
||||
|
||||
settings = GitHubProviderSettings.model_validate(
|
||||
provider_settings = GitHubDCRProviderSettings.model_validate(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
|
|
@ -241,20 +265,21 @@ class GitHubProvider(OAuthDCRProxy):
|
|||
)
|
||||
|
||||
# Validate required settings
|
||||
if not settings.client_id:
|
||||
if not provider_settings.client_id:
|
||||
raise ValueError(
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID"
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_DCR_CLIENT_ID"
|
||||
)
|
||||
if not settings.client_secret:
|
||||
if not provider_settings.client_secret:
|
||||
raise ValueError(
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET"
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_DCR_CLIENT_SECRET"
|
||||
)
|
||||
|
||||
# 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
|
||||
timeout_seconds_final = provider_settings.timeout_seconds or 10
|
||||
required_scopes_final = provider_settings.required_scopes or ["user"]
|
||||
allowed_client_redirect_uris_final = (
|
||||
provider_settings.allowed_client_redirect_uris
|
||||
)
|
||||
|
||||
# Create GitHub token verifier
|
||||
token_verifier = GitHubTokenVerifier(
|
||||
|
|
@ -264,26 +289,45 @@ class GitHubProvider(OAuthDCRProxy):
|
|||
|
||||
# Extract secret string from SecretStr
|
||||
client_secret_str = (
|
||||
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
||||
provider_settings.client_secret.get_secret_value()
|
||||
if provider_settings.client_secret
|
||||
else ""
|
||||
)
|
||||
|
||||
# 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_id=provider_settings.client_id,
|
||||
upstream_client_secret=client_secret_str,
|
||||
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
|
||||
base_url=provider_settings.base_url,
|
||||
redirect_path=provider_settings.redirect_path,
|
||||
issuer_url=provider_settings.issuer_url
|
||||
or provider_settings.base_url, # Default to base_url if not specified
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
client_storage=client_storage,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Initialized GitHub OAuth provider for client %s with scopes: %s",
|
||||
settings.client_id,
|
||||
"Initialized GitHub OAuth DCR provider for client %s with scopes: %s",
|
||||
provider_settings.client_id,
|
||||
required_scopes_final,
|
||||
)
|
||||
|
||||
|
||||
# Deprecated alias for backwards compatibility
|
||||
class GitHubProvider(GitHubDCRProvider):
|
||||
"""Deprecated: Use GitHubDCRProvider instead.
|
||||
|
||||
This alias is provided for backwards compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"GitHubProvider is deprecated, use GitHubDCRProvider instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ Google's OAuth flow, token validation, and user management.
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.google import GoogleProvider
|
||||
from fastmcp.server.auth.providers.google import GoogleDCRProvider
|
||||
|
||||
# Simple Google OAuth protection
|
||||
auth = GoogleProvider(
|
||||
auth = GoogleDCRProvider(
|
||||
client_id="your-google-client-id.apps.googleusercontent.com",
|
||||
client_secret="your-google-client-secret"
|
||||
)
|
||||
|
|
@ -22,16 +22,22 @@ Example:
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import warnings
|
||||
|
||||
import httpx
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from fastmcp.server.auth import TokenVerifier
|
||||
from fastmcp.server.auth.auth import AccessToken
|
||||
from fastmcp.server.auth.oauth_dcr_proxy import OAuthDCRProxy
|
||||
from fastmcp.settings import ENV_FILE
|
||||
from fastmcp.settings import (
|
||||
ENV_FILE,
|
||||
ExtendedEnvSettingsSource,
|
||||
ExtendedSettingsConfigDict,
|
||||
settings,
|
||||
)
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
|
@ -39,15 +45,32 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class GoogleProviderSettings(BaseSettings):
|
||||
"""Settings for Google OAuth provider."""
|
||||
class GoogleDCRProviderSettings(BaseSettings):
|
||||
"""Settings for Google OAuth DCR provider."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_GOOGLE_",
|
||||
model_config = ExtendedSettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_GOOGLE_DCR_",
|
||||
env_prefixes=["FASTMCP_SERVER_AUTH_GOOGLE_DCR_", "FASTMCP_SERVER_AUTH_GOOGLE_"],
|
||||
env_file=ENV_FILE,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls,
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
):
|
||||
return (
|
||||
init_settings,
|
||||
ExtendedEnvSettingsSource(settings_cls),
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
client_id: str | None = None
|
||||
client_secret: SecretStr | None = None
|
||||
base_url: AnyHttpUrl | str | None = None
|
||||
|
|
@ -182,8 +205,8 @@ class GoogleTokenVerifier(TokenVerifier):
|
|||
return None
|
||||
|
||||
|
||||
class GoogleProvider(OAuthDCRProxy):
|
||||
"""Complete Google OAuth provider for FastMCP.
|
||||
class GoogleDCRProvider(OAuthDCRProxy):
|
||||
"""Complete Google OAuth DCR provider for FastMCP.
|
||||
|
||||
This provider makes it trivial to add Google OAuth protection to any
|
||||
FastMCP server. Just provide your Google OAuth app credentials and
|
||||
|
|
@ -198,9 +221,9 @@ class GoogleProvider(OAuthDCRProxy):
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.google import GoogleProvider
|
||||
from fastmcp.server.auth.providers.google import GoogleDCRProvider
|
||||
|
||||
auth = GoogleProvider(
|
||||
auth = GoogleDCRProvider(
|
||||
client_id="123456789.apps.googleusercontent.com",
|
||||
client_secret="GOCSPX-abc123...",
|
||||
base_url="https://my-server.com"
|
||||
|
|
@ -242,7 +265,7 @@ class GoogleProvider(OAuthDCRProxy):
|
|||
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
||||
"""
|
||||
|
||||
settings = GoogleProviderSettings.model_validate(
|
||||
provider_settings = GoogleDCRProviderSettings.model_validate(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
|
|
@ -260,20 +283,22 @@ class GoogleProvider(OAuthDCRProxy):
|
|||
)
|
||||
|
||||
# Validate required settings
|
||||
if not settings.client_id:
|
||||
if not provider_settings.client_id:
|
||||
raise ValueError(
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID"
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_DCR_CLIENT_ID"
|
||||
)
|
||||
if not settings.client_secret:
|
||||
if not provider_settings.client_secret:
|
||||
raise ValueError(
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET"
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_DCR_CLIENT_SECRET"
|
||||
)
|
||||
|
||||
# Apply defaults
|
||||
timeout_seconds_final = settings.timeout_seconds or 10
|
||||
timeout_seconds_final = provider_settings.timeout_seconds or 10
|
||||
# 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 = provider_settings.required_scopes or ["openid"]
|
||||
allowed_client_redirect_uris_final = (
|
||||
provider_settings.allowed_client_redirect_uris
|
||||
)
|
||||
|
||||
# Create Google token verifier
|
||||
token_verifier = GoogleTokenVerifier(
|
||||
|
|
@ -283,26 +308,45 @@ class GoogleProvider(OAuthDCRProxy):
|
|||
|
||||
# Extract secret string from SecretStr
|
||||
client_secret_str = (
|
||||
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
||||
provider_settings.client_secret.get_secret_value()
|
||||
if provider_settings.client_secret
|
||||
else ""
|
||||
)
|
||||
|
||||
# Initialize OAuth proxy with Google endpoints
|
||||
super().__init__(
|
||||
upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
|
||||
upstream_token_endpoint="https://oauth2.googleapis.com/token",
|
||||
upstream_client_id=settings.client_id,
|
||||
upstream_client_id=provider_settings.client_id,
|
||||
upstream_client_secret=client_secret_str,
|
||||
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
|
||||
base_url=provider_settings.base_url,
|
||||
redirect_path=provider_settings.redirect_path,
|
||||
issuer_url=provider_settings.issuer_url
|
||||
or provider_settings.base_url, # Default to base_url if not specified
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
client_storage=client_storage,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Initialized Google OAuth provider for client %s with scopes: %s",
|
||||
settings.client_id,
|
||||
"Initialized Google OAuth DCR provider for client %s with scopes: %s",
|
||||
provider_settings.client_id,
|
||||
required_scopes_final,
|
||||
)
|
||||
|
||||
|
||||
# Deprecated alias for backwards compatibility
|
||||
class GoogleProvider(GoogleDCRProvider):
|
||||
"""Deprecated: Use GoogleDCRProvider instead.
|
||||
|
||||
This alias is provided for backwards compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"GoogleProvider is deprecated, use GoogleDCRProvider instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This module provides two WorkOS authentication strategies:
|
||||
|
||||
1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)
|
||||
1. WorkOSDCRProvider - OAuth DCR proxy for WorkOS Connect applications
|
||||
2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit
|
||||
|
||||
Choose based on your WorkOS setup and authentication requirements.
|
||||
|
|
@ -10,17 +10,24 @@ Choose based on your WorkOS setup and authentication requirements.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import httpx
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier
|
||||
from fastmcp.server.auth.oauth_dcr_proxy import OAuthDCRProxy
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
from fastmcp.settings import ENV_FILE
|
||||
from fastmcp.settings import (
|
||||
ENV_FILE,
|
||||
ExtendedEnvSettingsSource,
|
||||
ExtendedSettingsConfigDict,
|
||||
settings,
|
||||
)
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
|
@ -28,15 +35,32 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WorkOSProviderSettings(BaseSettings):
|
||||
"""Settings for WorkOS OAuth provider."""
|
||||
class WorkOSDCRProviderSettings(BaseSettings):
|
||||
"""Settings for WorkOS OAuth DCR provider."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_WORKOS_",
|
||||
model_config = ExtendedSettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_WORKOS_DCR_",
|
||||
env_prefixes=["FASTMCP_SERVER_AUTH_WORKOS_DCR_", "FASTMCP_SERVER_AUTH_WORKOS_"],
|
||||
env_file=ENV_FILE,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls,
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
):
|
||||
return (
|
||||
init_settings,
|
||||
ExtendedEnvSettingsSource(settings_cls),
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
client_id: str | None = None
|
||||
client_secret: SecretStr | None = None
|
||||
authkit_domain: str | None = None # e.g., "https://your-app.authkit.app"
|
||||
|
|
@ -125,14 +149,14 @@ class WorkOSTokenVerifier(TokenVerifier):
|
|||
return None
|
||||
|
||||
|
||||
class WorkOSProvider(OAuthDCRProxy):
|
||||
"""Complete WorkOS OAuth provider for FastMCP.
|
||||
class WorkOSDCRProvider(OAuthDCRProxy):
|
||||
"""Complete WorkOS OAuth DCR provider for FastMCP.
|
||||
|
||||
This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.
|
||||
This provider implements WorkOS AuthKit OAuth using the OAuth DCR Proxy pattern.
|
||||
It provides OAuth2 authentication for users through WorkOS Connect applications.
|
||||
|
||||
Features:
|
||||
- Transparent OAuth proxy to WorkOS AuthKit
|
||||
- Transparent OAuth DCR proxy to WorkOS AuthKit
|
||||
- Automatic token validation via userinfo endpoint
|
||||
- User information extraction from ID tokens
|
||||
- Support for standard OAuth scopes (openid, profile, email)
|
||||
|
|
@ -146,9 +170,9 @@ class WorkOSProvider(OAuthDCRProxy):
|
|||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.workos import WorkOSProvider
|
||||
from fastmcp.server.auth.providers.workos import WorkOSDCRProvider
|
||||
|
||||
auth = WorkOSProvider(
|
||||
auth = WorkOSDCRProvider(
|
||||
client_id="client_123",
|
||||
client_secret="sk_test_456",
|
||||
authkit_domain="https://your-app.authkit.app",
|
||||
|
|
@ -190,7 +214,7 @@ class WorkOSProvider(OAuthDCRProxy):
|
|||
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
||||
"""
|
||||
|
||||
settings = WorkOSProviderSettings.model_validate(
|
||||
provider_settings = WorkOSDCRProviderSettings.model_validate(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
|
|
@ -209,31 +233,35 @@ class WorkOSProvider(OAuthDCRProxy):
|
|||
)
|
||||
|
||||
# Validate required settings
|
||||
if not settings.client_id:
|
||||
if not provider_settings.client_id:
|
||||
raise ValueError(
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID"
|
||||
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_DCR_CLIENT_ID"
|
||||
)
|
||||
if not settings.client_secret:
|
||||
if not provider_settings.client_secret:
|
||||
raise ValueError(
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET"
|
||||
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_DCR_CLIENT_SECRET"
|
||||
)
|
||||
if not settings.authkit_domain:
|
||||
if not provider_settings.authkit_domain:
|
||||
raise ValueError(
|
||||
"authkit_domain is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN"
|
||||
"authkit_domain is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_DCR_AUTHKIT_DOMAIN"
|
||||
)
|
||||
|
||||
# Apply defaults and ensure authkit_domain is a full URL
|
||||
authkit_domain_str = settings.authkit_domain
|
||||
authkit_domain_str = provider_settings.authkit_domain
|
||||
if not authkit_domain_str.startswith(("http://", "https://")):
|
||||
authkit_domain_str = f"https://{authkit_domain_str}"
|
||||
authkit_domain_final = authkit_domain_str.rstrip("/")
|
||||
timeout_seconds_final = settings.timeout_seconds or 10
|
||||
scopes_final = settings.required_scopes or []
|
||||
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
||||
timeout_seconds_final = provider_settings.timeout_seconds or 10
|
||||
scopes_final = provider_settings.required_scopes or []
|
||||
allowed_client_redirect_uris_final = (
|
||||
provider_settings.allowed_client_redirect_uris
|
||||
)
|
||||
|
||||
# Extract secret string from SecretStr
|
||||
client_secret_str = (
|
||||
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
||||
provider_settings.client_secret.get_secret_value()
|
||||
if provider_settings.client_secret
|
||||
else ""
|
||||
)
|
||||
|
||||
# Create WorkOS token verifier
|
||||
|
|
@ -247,26 +275,43 @@ class WorkOSProvider(OAuthDCRProxy):
|
|||
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_id=provider_settings.client_id,
|
||||
upstream_client_secret=client_secret_str,
|
||||
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
|
||||
base_url=provider_settings.base_url,
|
||||
redirect_path=provider_settings.redirect_path,
|
||||
issuer_url=provider_settings.issuer_url
|
||||
or provider_settings.base_url, # Default to base_url if not specified
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
client_storage=client_storage,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
|
||||
settings.client_id,
|
||||
"Initialized WorkOS OAuth DCR provider for client %s with AuthKit domain %s",
|
||||
provider_settings.client_id,
|
||||
authkit_domain_final,
|
||||
)
|
||||
|
||||
|
||||
# Deprecated alias for backwards compatibility
|
||||
class WorkOSProvider(WorkOSDCRProvider):
|
||||
"""Deprecated: Use WorkOSDCRProvider instead.
|
||||
|
||||
This alias is provided for backwards compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"WorkOSProvider is deprecated, use WorkOSDCRProvider instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class AuthKitProviderSettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
model_config = ExtendedSettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
|
||||
env_file=ENV_FILE,
|
||||
extra="ignore",
|
||||
|
|
|
|||
63
tests/deprecated/test_oauth_dcr_providers.py
Normal file
63
tests/deprecated/test_oauth_dcr_providers.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""Test that deprecated provider imports still work.
|
||||
|
||||
This test file verifies that the old provider class names (without DCR suffix)
|
||||
can still be imported and are subclasses of the new DCR providers.
|
||||
"""
|
||||
|
||||
|
||||
class TestDeprecatedProviderImports:
|
||||
"""Test that deprecated provider names can be imported and are subclasses of DCR providers."""
|
||||
|
||||
def test_github_provider_import(self):
|
||||
"""Test that GitHubProvider can be imported and is a GitHubDCRProvider subclass."""
|
||||
from fastmcp.server.auth.providers.github import (
|
||||
GitHubDCRProvider,
|
||||
GitHubProvider,
|
||||
)
|
||||
|
||||
assert GitHubProvider is not None
|
||||
assert issubclass(GitHubProvider, GitHubDCRProvider)
|
||||
|
||||
def test_google_provider_import(self):
|
||||
"""Test that GoogleProvider can be imported and is a GoogleDCRProvider subclass."""
|
||||
from fastmcp.server.auth.providers.google import (
|
||||
GoogleDCRProvider,
|
||||
GoogleProvider,
|
||||
)
|
||||
|
||||
assert GoogleProvider is not None
|
||||
assert issubclass(GoogleProvider, GoogleDCRProvider)
|
||||
|
||||
def test_azure_provider_import(self):
|
||||
"""Test that AzureProvider can be imported and is an AzureDCRProvider subclass."""
|
||||
from fastmcp.server.auth.providers.azure import AzureDCRProvider, AzureProvider
|
||||
|
||||
assert AzureProvider is not None
|
||||
assert issubclass(AzureProvider, AzureDCRProvider)
|
||||
|
||||
def test_workos_provider_import(self):
|
||||
"""Test that WorkOSProvider can be imported and is a WorkOSDCRProvider subclass."""
|
||||
from fastmcp.server.auth.providers.workos import (
|
||||
WorkOSDCRProvider,
|
||||
WorkOSProvider,
|
||||
)
|
||||
|
||||
assert WorkOSProvider is not None
|
||||
assert issubclass(WorkOSProvider, WorkOSDCRProvider)
|
||||
|
||||
def test_auth0_provider_import(self):
|
||||
"""Test that Auth0Provider can be imported and is an Auth0DCRProvider subclass."""
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0DCRProvider, Auth0Provider
|
||||
|
||||
assert Auth0Provider is not None
|
||||
assert issubclass(Auth0Provider, Auth0DCRProvider)
|
||||
|
||||
def test_aws_cognito_provider_import(self):
|
||||
"""Test that AWSCognitoProvider can be imported and is an AWSCognitoDCRProvider subclass."""
|
||||
from fastmcp.server.auth.providers.aws import (
|
||||
AWSCognitoDCRProvider,
|
||||
AWSCognitoProvider,
|
||||
)
|
||||
|
||||
assert AWSCognitoProvider is not None
|
||||
assert issubclass(AWSCognitoProvider, AWSCognitoDCRProvider)
|
||||
|
|
@ -6,7 +6,10 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
from fastmcp.server.auth.oidc_dcr_proxy import OIDCConfiguration
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0Provider, Auth0ProviderSettings
|
||||
from fastmcp.server.auth.providers.auth0 import (
|
||||
Auth0DCRProvider,
|
||||
Auth0DCRProviderSettings,
|
||||
)
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
|
||||
TEST_CONFIG_URL = "https://example.com/.well-known/openid-configuration"
|
||||
|
|
@ -32,7 +35,7 @@ def valid_oidc_configuration_dict():
|
|||
}
|
||||
|
||||
|
||||
class TestAuth0ProviderSettings:
|
||||
class TestAuth0DCRProviderSettings:
|
||||
"""Test settings for Auth0 OAuth provider."""
|
||||
|
||||
def test_settings_from_env_vars(self):
|
||||
|
|
@ -40,18 +43,18 @@ class TestAuth0ProviderSettings:
|
|||
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(
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CONFIG_URL": TEST_CONFIG_URL,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_ID": TEST_CLIENT_ID,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_SECRET": TEST_CLIENT_SECRET,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_AUDIENCE": TEST_AUDIENCE,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_BASE_URL": TEST_BASE_URL,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_REDIRECT_PATH": TEST_REDIRECT_PATH,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_REQUIRED_SCOPES": ",".join(
|
||||
TEST_REQUIRED_SCOPES
|
||||
),
|
||||
},
|
||||
):
|
||||
settings = Auth0ProviderSettings()
|
||||
settings = Auth0DCRProviderSettings()
|
||||
|
||||
assert str(settings.config_url) == TEST_CONFIG_URL
|
||||
assert settings.client_id == TEST_CLIENT_ID
|
||||
|
|
@ -69,11 +72,11 @@ class TestAuth0ProviderSettings:
|
|||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID": TEST_CLIENT_ID,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET": TEST_CLIENT_SECRET,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_ID": TEST_CLIENT_ID,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_SECRET": TEST_CLIENT_SECRET,
|
||||
},
|
||||
):
|
||||
settings = Auth0ProviderSettings.model_validate(
|
||||
settings = Auth0DCRProviderSettings.model_validate(
|
||||
{
|
||||
"client_id": "explicit_client_id",
|
||||
"client_secret": "explicit_secret",
|
||||
|
|
@ -87,20 +90,20 @@ class TestAuth0ProviderSettings:
|
|||
)
|
||||
|
||||
|
||||
class TestAuth0Provider:
|
||||
"""Test Auth0Provider initialization."""
|
||||
class TestAuth0DCRProvider:
|
||||
"""Test Auth0DCRProvider initialization."""
|
||||
|
||||
def test_init_with_explicit_params(self, valid_oidc_configuration_dict):
|
||||
"""Test initialization with explicit parameters."""
|
||||
with patch(
|
||||
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration"
|
||||
"fastmcp.server.auth.oidc_dcr_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(
|
||||
provider = Auth0DCRProvider(
|
||||
config_url=TEST_CONFIG_URL,
|
||||
client_id=TEST_CLIENT_ID,
|
||||
client_secret=TEST_CLIENT_SECRET,
|
||||
|
|
@ -141,16 +144,16 @@ class TestAuth0Provider:
|
|||
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_DCR_CONFIG_URL": TEST_CONFIG_URL,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_ID": TEST_CLIENT_ID,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_SECRET": TEST_CLIENT_SECRET,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_AUDIENCE": TEST_AUDIENCE,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_BASE_URL": TEST_BASE_URL,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_REQUIRED_SCOPES": scopes_env,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration"
|
||||
"fastmcp.server.auth.oidc_dcr_proxy.OIDCConfiguration.get_oidc_configuration"
|
||||
) as mock_get,
|
||||
):
|
||||
oidc_config = OIDCConfiguration.model_validate(
|
||||
|
|
@ -158,7 +161,7 @@ class TestAuth0Provider:
|
|||
)
|
||||
mock_get.return_value = oidc_config
|
||||
|
||||
provider = Auth0Provider()
|
||||
provider = Auth0DCRProvider()
|
||||
|
||||
mock_get.assert_called_once()
|
||||
|
||||
|
|
@ -183,15 +186,15 @@ class TestAuth0Provider:
|
|||
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_DCR_CONFIG_URL": TEST_CONFIG_URL,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_ID": TEST_CLIENT_ID,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_CLIENT_SECRET": TEST_CLIENT_SECRET,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_AUDIENCE": TEST_AUDIENCE,
|
||||
"FASTMCP_SERVER_AUTH_AUTH0_DCR_BASE_URL": TEST_BASE_URL,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration"
|
||||
"fastmcp.server.auth.oidc_dcr_proxy.OIDCConfiguration.get_oidc_configuration"
|
||||
) as mock_get,
|
||||
):
|
||||
oidc_config = OIDCConfiguration.model_validate(
|
||||
|
|
@ -199,7 +202,7 @@ class TestAuth0Provider:
|
|||
)
|
||||
mock_get.return_value = oidc_config
|
||||
|
||||
provider = Auth0Provider(
|
||||
provider = Auth0DCRProvider(
|
||||
client_id="explicit_client",
|
||||
client_secret="explicit_secret",
|
||||
)
|
||||
|
|
@ -214,28 +217,28 @@ class TestAuth0Provider:
|
|||
# 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()
|
||||
Auth0DCRProvider()
|
||||
|
||||
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)
|
||||
Auth0DCRProvider(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)
|
||||
Auth0DCRProvider(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(
|
||||
Auth0DCRProvider(
|
||||
config_url=TEST_CONFIG_URL,
|
||||
client_id=TEST_CLIENT_ID,
|
||||
client_secret=TEST_CLIENT_SECRET,
|
||||
|
|
@ -246,7 +249,7 @@ class TestAuth0Provider:
|
|||
# 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(
|
||||
Auth0DCRProvider(
|
||||
config_url=TEST_CONFIG_URL,
|
||||
client_id=TEST_CLIENT_ID,
|
||||
client_secret=TEST_CLIENT_SECRET,
|
||||
|
|
@ -256,14 +259,14 @@ class TestAuth0Provider:
|
|||
def test_init_defaults(self, valid_oidc_configuration_dict):
|
||||
"""Test that default values are applied correctly."""
|
||||
with patch(
|
||||
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration"
|
||||
"fastmcp.server.auth.oidc_dcr_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(
|
||||
provider = Auth0DCRProvider(
|
||||
config_url=TEST_CONFIG_URL,
|
||||
client_id=TEST_CLIENT_ID,
|
||||
client_secret=TEST_CLIENT_SECRET,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
from fastmcp.server.auth.providers.aws import (
|
||||
AWSCognitoProvider,
|
||||
AWSCognitoProviderSettings,
|
||||
AWSCognitoDCRProvider,
|
||||
AWSCognitoDCRProviderSettings,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ def mock_cognito_oidc_discovery():
|
|||
yield
|
||||
|
||||
|
||||
class TestAWSCognitoProviderSettings:
|
||||
class TestAWSCognitoDCRProviderSettings:
|
||||
"""Test settings for AWS Cognito OAuth provider."""
|
||||
|
||||
def test_settings_from_env_vars(self):
|
||||
|
|
@ -46,15 +46,15 @@ class TestAWSCognitoProviderSettings:
|
|||
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",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_USER_POOL_ID": "us-east-1_XXXXXXXXX",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_AWS_REGION": "us-east-1",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_SECRET": "env_secret",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_BASE_URL": "https://example.com",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_REDIRECT_PATH": "/custom/callback",
|
||||
},
|
||||
):
|
||||
settings = AWSCognitoProviderSettings()
|
||||
settings = AWSCognitoDCRProviderSettings()
|
||||
|
||||
assert settings.user_pool_id == "us-east-1_XXXXXXXXX"
|
||||
assert settings.aws_region == "us-east-1"
|
||||
|
|
@ -71,12 +71,12 @@ class TestAWSCognitoProviderSettings:
|
|||
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_DCR_USER_POOL_ID": "env_pool_id",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_SECRET": "env_secret",
|
||||
},
|
||||
):
|
||||
settings = AWSCognitoProviderSettings.model_validate(
|
||||
settings = AWSCognitoDCRProviderSettings.model_validate(
|
||||
{
|
||||
"user_pool_id": "explicit_pool_id",
|
||||
"client_id": "explicit_client_id",
|
||||
|
|
@ -92,13 +92,13 @@ class TestAWSCognitoProviderSettings:
|
|||
)
|
||||
|
||||
|
||||
class TestAWSCognitoProvider:
|
||||
"""Test AWSCognitoProvider initialization."""
|
||||
class TestAWSCognitoDCRProvider:
|
||||
"""Test AWSCognitoDCRProvider initialization."""
|
||||
|
||||
def test_init_with_explicit_params(self):
|
||||
"""Test initialization with explicit parameters."""
|
||||
with mock_cognito_oidc_discovery():
|
||||
provider = AWSCognitoProvider(
|
||||
provider = AWSCognitoDCRProvider(
|
||||
user_pool_id="us-east-1_XXXXXXXXX",
|
||||
aws_region="us-east-1",
|
||||
client_id="test_client",
|
||||
|
|
@ -137,16 +137,16 @@ class TestAWSCognitoProvider:
|
|||
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_DCR_USER_POOL_ID": "us-east-1_XXXXXXXXX",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_AWS_REGION": "us-east-1",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_SECRET": "env_secret",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_BASE_URL": "https://env-example.com",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_REQUIRED_SCOPES": scopes_env,
|
||||
},
|
||||
):
|
||||
with mock_cognito_oidc_discovery():
|
||||
provider = AWSCognitoProvider()
|
||||
provider = AWSCognitoDCRProvider()
|
||||
|
||||
assert provider._upstream_client_id == "env_client_id"
|
||||
assert (
|
||||
|
|
@ -160,13 +160,13 @@ class TestAWSCognitoProvider:
|
|||
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_DCR_USER_POOL_ID": "env_pool_id",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_AWS_COGNITO_DCR_CLIENT_SECRET": "env_secret",
|
||||
},
|
||||
):
|
||||
with mock_cognito_oidc_discovery():
|
||||
provider = AWSCognitoProvider(
|
||||
provider = AWSCognitoDCRProvider(
|
||||
user_pool_id="explicit_pool_id",
|
||||
client_id="explicit_client",
|
||||
client_secret="explicit_secret",
|
||||
|
|
@ -185,7 +185,7 @@ class TestAWSCognitoProvider:
|
|||
"""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(
|
||||
AWSCognitoDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
)
|
||||
|
|
@ -194,7 +194,7 @@ class TestAWSCognitoProvider:
|
|||
"""Test that missing client_id raises ValueError."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(ValueError, match="client_id is required"):
|
||||
AWSCognitoProvider(
|
||||
AWSCognitoDCRProvider(
|
||||
user_pool_id="us-east-1_XXXXXXXXX",
|
||||
client_secret="test_secret",
|
||||
)
|
||||
|
|
@ -203,7 +203,7 @@ class TestAWSCognitoProvider:
|
|||
"""Test that missing client_secret raises ValueError."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(ValueError, match="client_secret is required"):
|
||||
AWSCognitoProvider(
|
||||
AWSCognitoDCRProvider(
|
||||
user_pool_id="us-east-1_XXXXXXXXX",
|
||||
client_id="test_client",
|
||||
)
|
||||
|
|
@ -211,7 +211,7 @@ class TestAWSCognitoProvider:
|
|||
def test_init_defaults(self):
|
||||
"""Test that default values are applied correctly."""
|
||||
with mock_cognito_oidc_discovery():
|
||||
provider = AWSCognitoProvider(
|
||||
provider = AWSCognitoDCRProvider(
|
||||
user_pool_id="us-east-1_XXXXXXXXX",
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
|
|
@ -227,7 +227,7 @@ class TestAWSCognitoProvider:
|
|||
def test_oidc_discovery_integration(self):
|
||||
"""Test that OIDC discovery endpoints are used correctly."""
|
||||
with mock_cognito_oidc_discovery():
|
||||
provider = AWSCognitoProvider(
|
||||
provider = AWSCognitoDCRProvider(
|
||||
user_pool_id="us-west-2_YYYYYYYY",
|
||||
aws_region="us-west-2",
|
||||
client_id="test_client",
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@ from mcp.server.auth.provider import AuthorizationParams
|
|||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider
|
||||
from fastmcp.server.auth.providers.azure import AzureDCRProvider
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
|
||||
|
||||
class TestAzureProvider:
|
||||
class TestAzureDCRProvider:
|
||||
"""Test Azure OAuth provider functionality."""
|
||||
|
||||
def test_init_with_explicit_params(self):
|
||||
"""Test AzureProvider initialization with explicit parameters."""
|
||||
provider = AzureProvider(
|
||||
"""Test AzureDCRProvider initialization with explicit parameters."""
|
||||
provider = AzureDCRProvider(
|
||||
client_id="12345678-1234-1234-1234-123456789012",
|
||||
client_secret="azure_secret_123",
|
||||
tenant_id="87654321-4321-4321-4321-210987654321",
|
||||
|
|
@ -43,18 +43,18 @@ class TestAzureProvider:
|
|||
],
|
||||
)
|
||||
def test_init_with_env_vars(self, scopes_env):
|
||||
"""Test AzureProvider initialization from environment variables."""
|
||||
"""Test AzureDCRProvider 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_DCR_CLIENT_ID": "env-client-id",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_DCR_CLIENT_SECRET": "env-secret",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_DCR_TENANT_ID": "env-tenant-id",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_DCR_BASE_URL": "https://envserver.com",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_DCR_REQUIRED_SCOPES": scopes_env,
|
||||
},
|
||||
):
|
||||
provider = AzureProvider()
|
||||
provider = AzureDCRProvider()
|
||||
|
||||
assert provider._upstream_client_id == "env-client-id"
|
||||
assert provider._upstream_client_secret.get_secret_value() == "env-secret"
|
||||
|
|
@ -72,7 +72,7 @@ class TestAzureProvider:
|
|||
def test_init_missing_client_id_raises_error(self):
|
||||
"""Test that missing client_id raises ValueError."""
|
||||
with pytest.raises(ValueError, match="client_id is required"):
|
||||
AzureProvider(
|
||||
AzureDCRProvider(
|
||||
client_secret="test_secret",
|
||||
tenant_id="test-tenant",
|
||||
)
|
||||
|
|
@ -80,7 +80,7 @@ class TestAzureProvider:
|
|||
def test_init_missing_client_secret_raises_error(self):
|
||||
"""Test that missing client_secret raises ValueError."""
|
||||
with pytest.raises(ValueError, match="client_secret is required"):
|
||||
AzureProvider(
|
||||
AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
tenant_id="test-tenant",
|
||||
)
|
||||
|
|
@ -88,14 +88,14 @@ class TestAzureProvider:
|
|||
def test_init_missing_tenant_id_raises_error(self):
|
||||
"""Test that missing tenant_id raises ValueError."""
|
||||
with pytest.raises(ValueError, match="tenant_id is required"):
|
||||
AzureProvider(
|
||||
AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
)
|
||||
|
||||
def test_init_defaults(self):
|
||||
"""Test that default values are applied correctly."""
|
||||
provider = AzureProvider(
|
||||
provider = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="test-tenant",
|
||||
|
|
@ -109,7 +109,7 @@ class TestAzureProvider:
|
|||
|
||||
def test_oauth_endpoints_configured_correctly(self):
|
||||
"""Test that OAuth endpoints are configured correctly."""
|
||||
provider = AzureProvider(
|
||||
provider = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="my-tenant-id",
|
||||
|
|
@ -133,7 +133,7 @@ class TestAzureProvider:
|
|||
def test_special_tenant_values(self):
|
||||
"""Test that special tenant values are accepted."""
|
||||
# Test with "organizations"
|
||||
provider1 = AzureProvider(
|
||||
provider1 = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="organizations",
|
||||
|
|
@ -143,7 +143,7 @@ class TestAzureProvider:
|
|||
assert "/organizations/" in parsed.path
|
||||
|
||||
# Test with "consumers"
|
||||
provider2 = AzureProvider(
|
||||
provider2 = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="consumers",
|
||||
|
|
@ -155,7 +155,7 @@ class TestAzureProvider:
|
|||
def test_azure_specific_scopes(self):
|
||||
"""Test handling of Azure-specific scope formats."""
|
||||
# Just test that the provider accepts Azure-specific scopes without error
|
||||
provider = AzureProvider(
|
||||
provider = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="test-tenant",
|
||||
|
|
@ -173,7 +173,7 @@ class TestAzureProvider:
|
|||
|
||||
def test_init_does_not_require_api_client_id_anymore(self):
|
||||
"""API client ID is no longer required; audience is client_id."""
|
||||
provider = AzureProvider(
|
||||
provider = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="test-tenant",
|
||||
|
|
@ -183,7 +183,7 @@ class TestAzureProvider:
|
|||
|
||||
def test_init_with_custom_audience_uses_jwt_verifier(self):
|
||||
"""When audience is provided, JWTVerifier is configured with JWKS and issuer."""
|
||||
provider = AzureProvider(
|
||||
provider = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="my-tenant",
|
||||
|
|
@ -204,7 +204,7 @@ class TestAzureProvider:
|
|||
@pytest.mark.asyncio
|
||||
async def test_authorize_filters_resource_and_prefixes_scopes_with_audience(self):
|
||||
"""authorize() should drop resource and prefix non-openid scopes with audience."""
|
||||
provider = AzureProvider(
|
||||
provider = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="common",
|
||||
|
|
@ -255,7 +255,7 @@ class TestAzureProvider:
|
|||
@pytest.mark.asyncio
|
||||
async def test_authorize_appends_unprefixed_additional_scopes(self):
|
||||
"""authorize() should append additional_authorize_scopes without prefixing them."""
|
||||
provider = AzureProvider(
|
||||
provider = AzureDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="common",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Unit tests for GitHub OAuth provider."""
|
||||
"""Unit tests for GitHub OAuth DCR provider."""
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
|
@ -6,28 +6,28 @@ from unittest.mock import MagicMock, patch
|
|||
import pytest
|
||||
|
||||
from fastmcp.server.auth.providers.github import (
|
||||
GitHubProvider,
|
||||
GitHubProviderSettings,
|
||||
GitHubDCRProvider,
|
||||
GitHubDCRProviderSettings,
|
||||
GitHubTokenVerifier,
|
||||
)
|
||||
|
||||
|
||||
class TestGitHubProviderSettings:
|
||||
"""Test settings for GitHub OAuth provider."""
|
||||
class TestGitHubDCRProviderSettings:
|
||||
"""Test settings for GitHub OAuth DCR 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_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_CLIENT_SECRET": "env_secret",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_BASE_URL": "https://example.com",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_REDIRECT_PATH": "/custom/callback",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_TIMEOUT_SECONDS": "30",
|
||||
},
|
||||
):
|
||||
settings = GitHubProviderSettings()
|
||||
settings = GitHubDCRProviderSettings()
|
||||
|
||||
assert settings.client_id == "env_client_id"
|
||||
assert (
|
||||
|
|
@ -43,11 +43,11 @@ class TestGitHubProviderSettings:
|
|||
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_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_CLIENT_SECRET": "env_secret",
|
||||
},
|
||||
):
|
||||
settings = GitHubProviderSettings.model_validate(
|
||||
settings = GitHubDCRProviderSettings.model_validate(
|
||||
{
|
||||
"client_id": "explicit_client_id",
|
||||
"client_secret": "explicit_secret",
|
||||
|
|
@ -61,12 +61,12 @@ class TestGitHubProviderSettings:
|
|||
)
|
||||
|
||||
|
||||
class TestGitHubProvider:
|
||||
"""Test GitHubProvider initialization."""
|
||||
class TestGitHubDCRProvider:
|
||||
"""Test GitHubDCRProvider initialization."""
|
||||
|
||||
def test_init_with_explicit_params(self):
|
||||
"""Test initialization with explicit parameters."""
|
||||
provider = GitHubProvider(
|
||||
provider = GitHubDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
base_url="https://example.com",
|
||||
|
|
@ -95,13 +95,13 @@ class TestGitHubProvider:
|
|||
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_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_CLIENT_SECRET": "env_secret",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_BASE_URL": "https://env-example.com",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_REQUIRED_SCOPES": scopes_env,
|
||||
},
|
||||
):
|
||||
provider = GitHubProvider()
|
||||
provider = GitHubDCRProvider()
|
||||
|
||||
assert provider._upstream_client_id == "env_client_id"
|
||||
assert provider._upstream_client_secret.get_secret_value() == "env_secret"
|
||||
|
|
@ -113,11 +113,11 @@ class TestGitHubProvider:
|
|||
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_DCR_CLIENT_ID": "env_client_id",
|
||||
"FASTMCP_SERVER_AUTH_GITHUB_DCR_CLIENT_SECRET": "env_secret",
|
||||
},
|
||||
):
|
||||
provider = GitHubProvider(
|
||||
provider = GitHubDCRProvider(
|
||||
client_id="explicit_client",
|
||||
client_secret="explicit_secret",
|
||||
)
|
||||
|
|
@ -132,18 +132,18 @@ class TestGitHubProvider:
|
|||
# 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")
|
||||
GitHubDCRProvider(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")
|
||||
GitHubDCRProvider(client_id="test_client")
|
||||
|
||||
def test_init_defaults(self):
|
||||
"""Test that default values are applied correctly."""
|
||||
provider = GitHubProvider(
|
||||
provider = GitHubDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from fastmcp.server.auth.providers.google import GoogleProvider
|
||||
from fastmcp.server.auth.providers.google import GoogleDCRProvider
|
||||
|
||||
|
||||
class TestGoogleProvider:
|
||||
class TestGoogleDCRProvider:
|
||||
"""Test Google OAuth provider functionality."""
|
||||
|
||||
def test_init_with_explicit_params(self):
|
||||
"""Test GoogleProvider initialization with explicit parameters."""
|
||||
provider = GoogleProvider(
|
||||
"""Test GoogleDCRProvider initialization with explicit parameters."""
|
||||
provider = GoogleDCRProvider(
|
||||
client_id="123456789.apps.googleusercontent.com",
|
||||
client_secret="GOCSPX-test123",
|
||||
base_url="https://myserver.com",
|
||||
|
|
@ -32,17 +32,17 @@ class TestGoogleProvider:
|
|||
],
|
||||
)
|
||||
def test_init_with_env_vars(self, scopes_env):
|
||||
"""Test GoogleProvider initialization from environment variables."""
|
||||
"""Test GoogleDCRProvider 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_DCR_CLIENT_ID": "env123.apps.googleusercontent.com",
|
||||
"FASTMCP_SERVER_AUTH_GOOGLE_DCR_CLIENT_SECRET": "GOCSPX-env456",
|
||||
"FASTMCP_SERVER_AUTH_GOOGLE_DCR_BASE_URL": "https://envserver.com",
|
||||
"FASTMCP_SERVER_AUTH_GOOGLE_DCR_REQUIRED_SCOPES": scopes_env,
|
||||
},
|
||||
):
|
||||
provider = GoogleProvider()
|
||||
provider = GoogleDCRProvider()
|
||||
|
||||
assert provider._upstream_client_id == "env123.apps.googleusercontent.com"
|
||||
assert (
|
||||
|
|
@ -59,18 +59,18 @@ class TestGoogleProvider:
|
|||
# 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")
|
||||
GoogleDCRProvider(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")
|
||||
GoogleDCRProvider(client_id="123456789.apps.googleusercontent.com")
|
||||
|
||||
def test_init_defaults(self):
|
||||
"""Test that default values are applied correctly."""
|
||||
provider = GoogleProvider(
|
||||
provider = GoogleDCRProvider(
|
||||
client_id="123456789.apps.googleusercontent.com",
|
||||
client_secret="GOCSPX-test123",
|
||||
)
|
||||
|
|
@ -82,7 +82,7 @@ class TestGoogleProvider:
|
|||
|
||||
def test_oauth_endpoints_configured_correctly(self):
|
||||
"""Test that OAuth endpoints are configured correctly."""
|
||||
provider = GoogleProvider(
|
||||
provider = GoogleDCRProvider(
|
||||
client_id="123456789.apps.googleusercontent.com",
|
||||
client_secret="GOCSPX-test123",
|
||||
base_url="https://myserver.com",
|
||||
|
|
@ -102,7 +102,7 @@ class TestGoogleProvider:
|
|||
def test_google_specific_scopes(self):
|
||||
"""Test handling of Google-specific scope formats."""
|
||||
# Just test that the provider accepts Google-specific scopes without error
|
||||
provider = GoogleProvider(
|
||||
provider = GoogleDCRProvider(
|
||||
client_id="123456789.apps.googleusercontent.com",
|
||||
client_secret="GOCSPX-test123",
|
||||
required_scopes=[
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@ import pytest
|
|||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
from fastmcp.server.auth.providers.workos import AuthKitProvider, WorkOSProvider
|
||||
from fastmcp.server.auth.providers.workos import AuthKitProvider, WorkOSDCRProvider
|
||||
from fastmcp.utilities.tests import HeadlessOAuth, run_server_async
|
||||
|
||||
|
||||
class TestWorkOSProvider:
|
||||
class TestWorkOSDCRProvider:
|
||||
"""Test WorkOS OAuth provider functionality."""
|
||||
|
||||
def test_init_with_explicit_params(self):
|
||||
"""Test WorkOSProvider initialization with explicit parameters."""
|
||||
provider = WorkOSProvider(
|
||||
"""Test WorkOSDCRProvider initialization with explicit parameters."""
|
||||
provider = WorkOSDCRProvider(
|
||||
client_id="client_test123",
|
||||
client_secret="secret_test456",
|
||||
authkit_domain="https://test.authkit.app",
|
||||
|
|
@ -38,18 +38,18 @@ class TestWorkOSProvider:
|
|||
],
|
||||
)
|
||||
def test_init_with_env_vars(self, scopes_env):
|
||||
"""Test WorkOSProvider initialization from environment variables."""
|
||||
"""Test WorkOSDCRProvider 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_DCR_CLIENT_ID": "env_client",
|
||||
"FASTMCP_SERVER_AUTH_WORKOS_DCR_CLIENT_SECRET": "env_secret",
|
||||
"FASTMCP_SERVER_AUTH_WORKOS_DCR_AUTHKIT_DOMAIN": "https://env.authkit.app",
|
||||
"FASTMCP_SERVER_AUTH_WORKOS_DCR_BASE_URL": "https://envserver.com",
|
||||
"FASTMCP_SERVER_AUTH_WORKOS_DCR_REQUIRED_SCOPES": scopes_env,
|
||||
},
|
||||
):
|
||||
provider = WorkOSProvider()
|
||||
provider = WorkOSDCRProvider()
|
||||
|
||||
assert provider._upstream_client_id == "env_client"
|
||||
assert provider._upstream_client_secret.get_secret_value() == "env_secret"
|
||||
|
|
@ -62,7 +62,7 @@ class TestWorkOSProvider:
|
|||
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(
|
||||
WorkOSDCRProvider(
|
||||
client_secret="test_secret",
|
||||
authkit_domain="https://test.authkit.app",
|
||||
)
|
||||
|
|
@ -70,7 +70,7 @@ class TestWorkOSProvider:
|
|||
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(
|
||||
WorkOSDCRProvider(
|
||||
client_id="test_client",
|
||||
authkit_domain="https://test.authkit.app",
|
||||
)
|
||||
|
|
@ -78,7 +78,7 @@ class TestWorkOSProvider:
|
|||
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(
|
||||
WorkOSDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
)
|
||||
|
|
@ -86,7 +86,7 @@ class TestWorkOSProvider:
|
|||
def test_authkit_domain_https_prefix_handling(self):
|
||||
"""Test that authkit_domain handles missing https:// prefix."""
|
||||
# Without https:// - should add it
|
||||
provider1 = WorkOSProvider(
|
||||
provider1 = WorkOSDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
authkit_domain="test.authkit.app",
|
||||
|
|
@ -98,7 +98,7 @@ class TestWorkOSProvider:
|
|||
assert parsed.path == "/oauth2/authorize"
|
||||
|
||||
# With https:// - should keep it
|
||||
provider2 = WorkOSProvider(
|
||||
provider2 = WorkOSDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
authkit_domain="https://test.authkit.app",
|
||||
|
|
@ -110,7 +110,7 @@ class TestWorkOSProvider:
|
|||
assert parsed.path == "/oauth2/authorize"
|
||||
|
||||
# With http:// - should be preserved
|
||||
provider3 = WorkOSProvider(
|
||||
provider3 = WorkOSDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
authkit_domain="http://localhost:8080",
|
||||
|
|
@ -123,7 +123,7 @@ class TestWorkOSProvider:
|
|||
|
||||
def test_init_defaults(self):
|
||||
"""Test that default values are applied correctly."""
|
||||
provider = WorkOSProvider(
|
||||
provider = WorkOSDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
authkit_domain="https://test.authkit.app",
|
||||
|
|
@ -136,7 +136,7 @@ class TestWorkOSProvider:
|
|||
|
||||
def test_oauth_endpoints_configured_correctly(self):
|
||||
"""Test that OAuth endpoints are configured correctly."""
|
||||
provider = WorkOSProvider(
|
||||
provider = WorkOSDCRProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
authkit_domain="https://test.authkit.app",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue