mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-12 00:29:11 +02:00
322 lines
10 KiB
Python
322 lines
10 KiB
Python
"""Tests for first-party auth plugin wrappers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.server.auth.oidc_proxy import OIDCConfiguration
|
|
from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
|
|
from fastmcp.server.plugins.auth.auth0 import Auth0Auth
|
|
from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
|
|
from fastmcp.server.plugins.auth.authkit import AuthKitAuth
|
|
from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
|
|
from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
|
|
from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
|
|
from fastmcp.server.plugins.auth.azure import AzureAuth
|
|
from fastmcp.server.plugins.auth.azure.provider import AzureProvider
|
|
from fastmcp.server.plugins.auth.clerk import ClerkAuth
|
|
from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
|
|
from fastmcp.server.plugins.auth.descope import DescopeAuth
|
|
from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
|
|
from fastmcp.server.plugins.auth.discord import DiscordAuth
|
|
from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
|
|
from fastmcp.server.plugins.auth.github import GitHubAuth
|
|
from fastmcp.server.plugins.auth.github.provider import GitHubProvider
|
|
from fastmcp.server.plugins.auth.google import GoogleAuth
|
|
from fastmcp.server.plugins.auth.google.provider import GoogleProvider
|
|
from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
|
|
from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
|
|
from fastmcp.server.plugins.auth.oci import OCIAuth
|
|
from fastmcp.server.plugins.auth.oci.provider import OCIProvider
|
|
from fastmcp.server.plugins.auth.propelauth import PropelAuth
|
|
from fastmcp.server.plugins.auth.propelauth.provider import PropelAuthProvider
|
|
from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
|
|
from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
|
|
from fastmcp.server.plugins.auth.supabase import SupabaseAuth
|
|
from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
|
|
from fastmcp.server.plugins.auth.workos import WorkOSAuth
|
|
from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
|
|
|
|
|
|
def _verifier() -> StaticTokenVerifier:
|
|
return StaticTokenVerifier(tokens={"t": {"client_id": "c", "scopes": []}})
|
|
|
|
|
|
def _oidc_config() -> OIDCConfiguration:
|
|
return OIDCConfiguration.model_validate(
|
|
{
|
|
"issuer": "https://idp.example.com",
|
|
"authorization_endpoint": "https://idp.example.com/authorize",
|
|
"token_endpoint": "https://idp.example.com/token",
|
|
"jwks_uri": "https://idp.example.com/jwks.json",
|
|
"response_types_supported": ["code"],
|
|
"subject_types_supported": ["public"],
|
|
"id_token_signing_alg_values_supported": ["RS256"],
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _mock_oidc_discovery():
|
|
with patch(
|
|
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration",
|
|
return_value=_oidc_config(),
|
|
):
|
|
yield
|
|
|
|
|
|
PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
|
|
(
|
|
Auth0Auth,
|
|
Auth0Auth.Config,
|
|
{
|
|
"config_url": "https://idp.example.com/.well-known/openid-configuration",
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"audience": "audience",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
Auth0Provider,
|
|
),
|
|
(
|
|
AuthKitAuth,
|
|
AuthKitAuth.Config,
|
|
{
|
|
"authkit_domain": "https://example.authkit.app",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
AuthKitProvider,
|
|
),
|
|
(
|
|
AWSCognitoAuth,
|
|
AWSCognitoAuth.Config,
|
|
{
|
|
"user_pool_id": "us-east-1_abc",
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"aws_region": "us-east-1",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
AWSCognitoProvider,
|
|
),
|
|
(
|
|
AzureAuth,
|
|
AzureAuth.Config,
|
|
{
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"tenant_id": "tenant",
|
|
"required_scopes": ["read"],
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
AzureProvider,
|
|
),
|
|
(
|
|
ClerkAuth,
|
|
ClerkAuth.Config,
|
|
{
|
|
"domain": "example.clerk.accounts.dev",
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
ClerkProvider,
|
|
),
|
|
(
|
|
DescopeAuth,
|
|
DescopeAuth.Config,
|
|
{
|
|
"config_url": "https://api.descope.com/v1/apps/agentic/P123/M456/.well-known/openid-configuration",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
DescopeProvider,
|
|
),
|
|
(
|
|
DiscordAuth,
|
|
DiscordAuth.Config,
|
|
{
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
DiscordProvider,
|
|
),
|
|
(
|
|
GitHubAuth,
|
|
GitHubAuth.Config,
|
|
{
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
GitHubProvider,
|
|
),
|
|
(
|
|
GoogleAuth,
|
|
GoogleAuth.Config,
|
|
{
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
GoogleProvider,
|
|
),
|
|
(
|
|
KeycloakAuth,
|
|
KeycloakAuth.Config,
|
|
{
|
|
"realm_url": "https://keycloak.example.com/realms/main",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
KeycloakAuthProvider,
|
|
),
|
|
(
|
|
OCIAuth,
|
|
OCIAuth.Config,
|
|
{
|
|
"config_url": "https://idp.example.com/.well-known/openid-configuration",
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
OCIProvider,
|
|
),
|
|
(
|
|
PropelAuth,
|
|
PropelAuth.Config,
|
|
{
|
|
"auth_url": "https://auth.example.com",
|
|
"introspection_client_id": "client",
|
|
"introspection_client_secret": "secret",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
PropelAuthProvider,
|
|
),
|
|
(
|
|
ScalekitAuth,
|
|
ScalekitAuth.Config,
|
|
{
|
|
"environment_url": "https://env.scalekit.com",
|
|
"resource_id": "res_123",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
ScalekitProvider,
|
|
),
|
|
(
|
|
SupabaseAuth,
|
|
SupabaseAuth.Config,
|
|
{
|
|
"project_url": "https://abc123.supabase.co",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
SupabaseProvider,
|
|
),
|
|
(
|
|
WorkOSAuth,
|
|
WorkOSAuth.Config,
|
|
{
|
|
"client_id": "client",
|
|
"client_secret": "secret",
|
|
"authkit_domain": "https://example.authkit.app",
|
|
"base_url": "https://mcp.example.com",
|
|
},
|
|
WorkOSProvider,
|
|
),
|
|
]
|
|
|
|
|
|
def _plugin_kwargs(plugin_cls: type) -> dict[str, Any]:
|
|
if plugin_cls in {
|
|
AuthKitAuth,
|
|
DescopeAuth,
|
|
KeycloakAuth,
|
|
ScalekitAuth,
|
|
SupabaseAuth,
|
|
}:
|
|
return {"token_verifier": _verifier()}
|
|
return {}
|
|
|
|
|
|
class TestAuthProviderPlugins:
|
|
@pytest.mark.parametrize(
|
|
("plugin_cls", "config_cls", "config", "provider_cls"), PROVIDER_CASES
|
|
)
|
|
def test_config_generic_binding(self, plugin_cls, config_cls, config, provider_cls):
|
|
assert plugin_cls._config_cls is config_cls
|
|
assert plugin_cls.Config is config_cls
|
|
|
|
@pytest.mark.parametrize(
|
|
("plugin_cls", "config_cls", "config", "provider_cls"), PROVIDER_CASES
|
|
)
|
|
def test_default_config_instantiable(
|
|
self, plugin_cls, config_cls, config, provider_cls
|
|
):
|
|
assert config_cls()
|
|
|
|
@pytest.mark.parametrize(
|
|
("plugin_cls", "config_cls", "config", "provider_cls"), PROVIDER_CASES
|
|
)
|
|
def test_unknown_config_key_rejected(
|
|
self, plugin_cls, config_cls, config, provider_cls
|
|
):
|
|
with pytest.raises((ValidationError, Exception), match="forbid|extra"):
|
|
config_cls(not_a_real_option=True)
|
|
|
|
@pytest.mark.parametrize(
|
|
("plugin_cls", "config_cls", "config", "provider_cls"), PROVIDER_CASES
|
|
)
|
|
def test_auth_builds_provider(self, plugin_cls, config_cls, config, provider_cls):
|
|
auth = plugin_cls(config, **_plugin_kwargs(plugin_cls)).auth()
|
|
|
|
assert isinstance(auth, provider_cls)
|
|
|
|
@pytest.mark.parametrize(
|
|
("plugin_cls", "config_cls", "config", "provider_cls"), PROVIDER_CASES
|
|
)
|
|
def test_plugin_installs_as_server_auth(
|
|
self, plugin_cls, config_cls, config, provider_cls
|
|
):
|
|
plugin = plugin_cls(config, **_plugin_kwargs(plugin_cls))
|
|
|
|
mcp = FastMCP("t", plugins=[plugin])
|
|
|
|
assert isinstance(mcp.auth, provider_cls)
|
|
|
|
@pytest.mark.parametrize("missing", ["project_url", "base_url"])
|
|
def test_required_fields_checked_when_auth_builds(self, missing: str):
|
|
config = {
|
|
"project_url": "https://abc123.supabase.co",
|
|
"base_url": "https://mcp.example.com",
|
|
}
|
|
del config[missing]
|
|
|
|
plugin = SupabaseAuth(config, token_verifier=_verifier())
|
|
|
|
with pytest.raises(ValueError, match=missing):
|
|
plugin.auth()
|
|
|
|
def test_supabase_passthroughs_config_and_python_verifier(self):
|
|
verifier = _verifier()
|
|
plugin = SupabaseAuth(
|
|
SupabaseAuth.Config(
|
|
project_url="https://abc123.supabase.co",
|
|
base_url="https://mcp.example.com",
|
|
required_scopes=["read"],
|
|
scopes_supported=["read", "write"],
|
|
resource_name="Example MCP",
|
|
),
|
|
token_verifier=verifier,
|
|
)
|
|
|
|
auth = plugin.auth()
|
|
|
|
assert isinstance(auth, SupabaseProvider)
|
|
assert auth.token_verifier is verifier
|
|
assert str(auth.base_url).rstrip("/") == "https://mcp.example.com"
|
|
assert auth._scopes_supported == ["read", "write"]
|
|
assert auth.resource_name == "Example MCP"
|