From 0d8bb8cdd638d2fef3e360d974224a7b38f43659 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 1 Jun 2025 16:45:31 -0400 Subject: [PATCH] flatten auth settings --- src/fastmcp/server/auth/auth.py | 13 ++++----- src/fastmcp/server/auth/providers/bearer.py | 3 +- .../server/auth/providers/bearer_env.py | 29 ++++++++++++++----- src/fastmcp/server/http.py | 10 +++---- tests/auth/providers/test_bearer_env.py | 19 ++++++------ 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/fastmcp/server/auth/auth.py b/src/fastmcp/server/auth/auth.py index d4ae8c821..42d2919b8 100644 --- a/src/fastmcp/server/auth/auth.py +++ b/src/fastmcp/server/auth/auth.py @@ -5,7 +5,6 @@ from mcp.server.auth.provider import ( RefreshToken, ) from mcp.server.auth.settings import ( - AuthSettings, ClientRegistrationOptions, RevocationOptions, ) @@ -39,10 +38,8 @@ class OAuthProvider( if isinstance(service_documentation_url, str): service_documentation_url = AnyHttpUrl(service_documentation_url) - self.auth_settings = AuthSettings( - issuer_url=issuer_url, - service_documentation_url=service_documentation_url, - client_registration_options=client_registration_options, - revocation_options=revocation_options, - required_scopes=required_scopes, - ) + self.issuer_url = issuer_url + self.service_documentation_url = service_documentation_url + self.client_registration_options = client_registration_options + self.revocation_options = revocation_options + self.required_scopes = required_scopes diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index a463817e6..763f90f4f 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -295,7 +295,8 @@ class BearerAuthProvider(OAuthProvider): if exp and exp < time.time(): return None - # Validate issuer + # Validate issuer - note we use issuer instead of issuer_url here because + # issuer is optional, allowing users to make this check optional if self.issuer: if claims.get("iss") != self.issuer: return None diff --git a/src/fastmcp/server/auth/providers/bearer_env.py b/src/fastmcp/server/auth/providers/bearer_env.py index ff02af62f..96cf15cfa 100644 --- a/src/fastmcp/server/auth/providers/bearer_env.py +++ b/src/fastmcp/server/auth/providers/bearer_env.py @@ -3,7 +3,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from fastmcp.server.auth.providers.bearer import BearerAuthProvider -class NotSet: +# Sentinel object to indicate that a setting is not set +class _NotSet: pass @@ -25,17 +26,29 @@ class EnvBearerAuthProviderSettings(BaseSettings): class EnvBearerAuthProvider(BearerAuthProvider): """ - A BearerAuthProvider that loads settings from environment variables. + A BearerAuthProvider that loads settings from environment variables. Any + providing setting will always take precedence over the environment + variables. """ def __init__( self, - public_key: str | None | type[NotSet] = NotSet, - jwks_uri: str | None | type[NotSet] = NotSet, - issuer: str | None | type[NotSet] = NotSet, - audience: str | None | type[NotSet] = NotSet, - required_scopes: list[str] | None | type[NotSet] = NotSet, + public_key: str | None | type[_NotSet] = _NotSet, + jwks_uri: str | None | type[_NotSet] = _NotSet, + issuer: str | None | type[_NotSet] = _NotSet, + audience: str | None | type[_NotSet] = _NotSet, + required_scopes: list[str] | None | type[_NotSet] = _NotSet, ): + """ + Initialize the provider. + + Args: + public_key: RSA public key in PEM format (for static key) + jwks_uri: URI to fetch keys from (for key rotation) + issuer: Expected issuer claim (optional) + audience: Expected audience claim (optional) + required_scopes: List of required scopes for access (optional) + """ kwargs = { "public_key": public_key, "jwks_uri": jwks_uri, @@ -44,6 +57,6 @@ class EnvBearerAuthProvider(BearerAuthProvider): "required_scopes": required_scopes, } settings = EnvBearerAuthProviderSettings( - **{k: v for k, v in kwargs.items() if v is not NotSet} + **{k: v for k, v in kwargs.items() if v is not _NotSet} ) super().__init__(**settings.model_dump()) diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index d9530adb1..2b5381437 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -91,15 +91,15 @@ def setup_auth_middleware_and_routes( Middleware(AuthContextMiddleware), ] - required_scopes = auth.auth_settings.required_scopes or [] + required_scopes = auth.required_scopes or [] auth_routes.extend( create_auth_routes( provider=auth, - issuer_url=auth.auth_settings.issuer_url, - service_documentation_url=auth.auth_settings.service_documentation_url, - client_registration_options=auth.auth_settings.client_registration_options, - revocation_options=auth.auth_settings.revocation_options, + issuer_url=auth.issuer_url, + service_documentation_url=auth.service_documentation_url, + client_registration_options=auth.client_registration_options, + revocation_options=auth.revocation_options, ) ) diff --git a/tests/auth/providers/test_bearer_env.py b/tests/auth/providers/test_bearer_env.py index 7c81ba737..cadf7075a 100644 --- a/tests/auth/providers/test_bearer_env.py +++ b/tests/auth/providers/test_bearer_env.py @@ -10,7 +10,7 @@ def test_load_bearer_env_from_env_var(monkeypatch): mcp = FastMCP() assert mcp.auth is None - monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env") + monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") mcp_with_auth = FastMCP() @@ -21,7 +21,7 @@ def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatc mcp = FastMCP() assert mcp.auth is None - monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env") + monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") with pytest.raises( ValueError, match="Either public_key or jwks_uri must be provided" @@ -30,7 +30,7 @@ def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatc def test_configure_bearer_env_from_env_var(monkeypatch): - monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env") + monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") monkeypatch.setenv("FASTMCP_AUTH_BEARER_ISSUER", "http://test-issuer") monkeypatch.setenv("FASTMCP_AUTH_BEARER_AUDIENCE", "test-audience") @@ -41,14 +41,13 @@ def test_configure_bearer_env_from_env_var(monkeypatch): mcp = FastMCP() assert isinstance(mcp.auth, EnvBearerAuthProvider) assert mcp.auth.public_key == "test-public-key" - assert mcp.auth.issuer == "http://test-issuer" - assert mcp.auth.auth_settings.issuer_url == AnyHttpUrl("http://test-issuer") + assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer") assert mcp.auth.audience == "test-audience" - assert mcp.auth.auth_settings.required_scopes == ["test-scope1", "test-scope2"] + assert mcp.auth.required_scopes == ["test-scope1", "test-scope2"] def test_list_of_scopes_must_be_a_list(monkeypatch): - monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env") + monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1") with pytest.raises(ValidationError, match="Input should be a valid list"): @@ -56,7 +55,7 @@ def test_list_of_scopes_must_be_a_list(monkeypatch): def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch): - monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env") + monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri") mcp = FastMCP() @@ -65,7 +64,7 @@ def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch): def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch): - monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env") + monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri") @@ -74,7 +73,7 @@ def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch): def test_provided_auth_takes_precedence_over_env_vars(monkeypatch): - monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env") + monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env") monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key") mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))