Jeremiah Lowin commited on
Commit
058fad1
·
1 Parent(s): ccc6631

flatten auth settings

Browse files
src/fastmcp/server/auth/auth.py CHANGED
@@ -5,7 +5,6 @@ from mcp.server.auth.provider import (
5
  RefreshToken,
6
  )
7
  from mcp.server.auth.settings import (
8
- AuthSettings,
9
  ClientRegistrationOptions,
10
  RevocationOptions,
11
  )
@@ -39,10 +38,8 @@ class OAuthProvider(
39
  if isinstance(service_documentation_url, str):
40
  service_documentation_url = AnyHttpUrl(service_documentation_url)
41
 
42
- self.auth_settings = AuthSettings(
43
- issuer_url=issuer_url,
44
- service_documentation_url=service_documentation_url,
45
- client_registration_options=client_registration_options,
46
- revocation_options=revocation_options,
47
- required_scopes=required_scopes,
48
- )
 
5
  RefreshToken,
6
  )
7
  from mcp.server.auth.settings import (
 
8
  ClientRegistrationOptions,
9
  RevocationOptions,
10
  )
 
38
  if isinstance(service_documentation_url, str):
39
  service_documentation_url = AnyHttpUrl(service_documentation_url)
40
 
41
+ self.issuer_url = issuer_url
42
+ self.service_documentation_url = service_documentation_url
43
+ self.client_registration_options = client_registration_options
44
+ self.revocation_options = revocation_options
45
+ self.required_scopes = required_scopes
 
 
src/fastmcp/server/auth/providers/bearer.py CHANGED
@@ -295,7 +295,8 @@ class BearerAuthProvider(OAuthProvider):
295
  if exp and exp < time.time():
296
  return None
297
 
298
- # Validate issuer
 
299
  if self.issuer:
300
  if claims.get("iss") != self.issuer:
301
  return None
 
295
  if exp and exp < time.time():
296
  return None
297
 
298
+ # Validate issuer - note we use issuer instead of issuer_url here because
299
+ # issuer is optional, allowing users to make this check optional
300
  if self.issuer:
301
  if claims.get("iss") != self.issuer:
302
  return None
src/fastmcp/server/auth/providers/bearer_env.py CHANGED
@@ -3,7 +3,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
3
  from fastmcp.server.auth.providers.bearer import BearerAuthProvider
4
 
5
 
6
- class NotSet:
 
7
  pass
8
 
9
 
@@ -25,17 +26,29 @@ class EnvBearerAuthProviderSettings(BaseSettings):
25
 
26
  class EnvBearerAuthProvider(BearerAuthProvider):
27
  """
28
- A BearerAuthProvider that loads settings from environment variables.
 
 
29
  """
30
 
31
  def __init__(
32
  self,
33
- public_key: str | None | type[NotSet] = NotSet,
34
- jwks_uri: str | None | type[NotSet] = NotSet,
35
- issuer: str | None | type[NotSet] = NotSet,
36
- audience: str | None | type[NotSet] = NotSet,
37
- required_scopes: list[str] | None | type[NotSet] = NotSet,
38
  ):
 
 
 
 
 
 
 
 
 
 
39
  kwargs = {
40
  "public_key": public_key,
41
  "jwks_uri": jwks_uri,
@@ -44,6 +57,6 @@ class EnvBearerAuthProvider(BearerAuthProvider):
44
  "required_scopes": required_scopes,
45
  }
46
  settings = EnvBearerAuthProviderSettings(
47
- **{k: v for k, v in kwargs.items() if v is not NotSet}
48
  )
49
  super().__init__(**settings.model_dump())
 
3
  from fastmcp.server.auth.providers.bearer import BearerAuthProvider
4
 
5
 
6
+ # Sentinel object to indicate that a setting is not set
7
+ class _NotSet:
8
  pass
9
 
10
 
 
26
 
27
  class EnvBearerAuthProvider(BearerAuthProvider):
28
  """
29
+ A BearerAuthProvider that loads settings from environment variables. Any
30
+ providing setting will always take precedence over the environment
31
+ variables.
32
  """
33
 
34
  def __init__(
35
  self,
36
+ public_key: str | None | type[_NotSet] = _NotSet,
37
+ jwks_uri: str | None | type[_NotSet] = _NotSet,
38
+ issuer: str | None | type[_NotSet] = _NotSet,
39
+ audience: str | None | type[_NotSet] = _NotSet,
40
+ required_scopes: list[str] | None | type[_NotSet] = _NotSet,
41
  ):
42
+ """
43
+ Initialize the provider.
44
+
45
+ Args:
46
+ public_key: RSA public key in PEM format (for static key)
47
+ jwks_uri: URI to fetch keys from (for key rotation)
48
+ issuer: Expected issuer claim (optional)
49
+ audience: Expected audience claim (optional)
50
+ required_scopes: List of required scopes for access (optional)
51
+ """
52
  kwargs = {
53
  "public_key": public_key,
54
  "jwks_uri": jwks_uri,
 
57
  "required_scopes": required_scopes,
58
  }
59
  settings = EnvBearerAuthProviderSettings(
60
+ **{k: v for k, v in kwargs.items() if v is not _NotSet}
61
  )
62
  super().__init__(**settings.model_dump())
src/fastmcp/server/http.py CHANGED
@@ -91,15 +91,15 @@ def setup_auth_middleware_and_routes(
91
  Middleware(AuthContextMiddleware),
92
  ]
93
 
94
- required_scopes = auth.auth_settings.required_scopes or []
95
 
96
  auth_routes.extend(
97
  create_auth_routes(
98
  provider=auth,
99
- issuer_url=auth.auth_settings.issuer_url,
100
- service_documentation_url=auth.auth_settings.service_documentation_url,
101
- client_registration_options=auth.auth_settings.client_registration_options,
102
- revocation_options=auth.auth_settings.revocation_options,
103
  )
104
  )
105
 
 
91
  Middleware(AuthContextMiddleware),
92
  ]
93
 
94
+ required_scopes = auth.required_scopes or []
95
 
96
  auth_routes.extend(
97
  create_auth_routes(
98
  provider=auth,
99
+ issuer_url=auth.issuer_url,
100
+ service_documentation_url=auth.service_documentation_url,
101
+ client_registration_options=auth.client_registration_options,
102
+ revocation_options=auth.revocation_options,
103
  )
104
  )
105
 
tests/auth/providers/test_bearer_env.py CHANGED
@@ -10,7 +10,7 @@ def test_load_bearer_env_from_env_var(monkeypatch):
10
  mcp = FastMCP()
11
  assert mcp.auth is None
12
 
13
- monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env")
14
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
15
 
16
  mcp_with_auth = FastMCP()
@@ -21,7 +21,7 @@ def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatc
21
  mcp = FastMCP()
22
  assert mcp.auth is None
23
 
24
- monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env")
25
 
26
  with pytest.raises(
27
  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
30
 
31
 
32
  def test_configure_bearer_env_from_env_var(monkeypatch):
33
- monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env")
34
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
35
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_ISSUER", "http://test-issuer")
36
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_AUDIENCE", "test-audience")
@@ -41,14 +41,13 @@ def test_configure_bearer_env_from_env_var(monkeypatch):
41
  mcp = FastMCP()
42
  assert isinstance(mcp.auth, EnvBearerAuthProvider)
43
  assert mcp.auth.public_key == "test-public-key"
44
- assert mcp.auth.issuer == "http://test-issuer"
45
- assert mcp.auth.auth_settings.issuer_url == AnyHttpUrl("http://test-issuer")
46
  assert mcp.auth.audience == "test-audience"
47
- assert mcp.auth.auth_settings.required_scopes == ["test-scope1", "test-scope2"]
48
 
49
 
50
  def test_list_of_scopes_must_be_a_list(monkeypatch):
51
- monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env")
52
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
53
 
54
  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):
56
 
57
 
58
  def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
59
- monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env")
60
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
61
 
62
  mcp = FastMCP()
@@ -65,7 +64,7 @@ def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
65
 
66
 
67
  def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
68
- monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env")
69
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
70
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
71
 
@@ -74,7 +73,7 @@ def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
74
 
75
 
76
  def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
77
- monkeypatch.setenv("FASTMCP_SERVER_AUTH_PROVIDER", "bearer_env")
78
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
79
 
80
  mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))
 
10
  mcp = FastMCP()
11
  assert mcp.auth is None
12
 
13
+ monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
14
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
15
 
16
  mcp_with_auth = FastMCP()
 
21
  mcp = FastMCP()
22
  assert mcp.auth is None
23
 
24
+ monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
25
 
26
  with pytest.raises(
27
  ValueError, match="Either public_key or jwks_uri must be provided"
 
30
 
31
 
32
  def test_configure_bearer_env_from_env_var(monkeypatch):
33
+ monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
34
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
35
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_ISSUER", "http://test-issuer")
36
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_AUDIENCE", "test-audience")
 
41
  mcp = FastMCP()
42
  assert isinstance(mcp.auth, EnvBearerAuthProvider)
43
  assert mcp.auth.public_key == "test-public-key"
44
+ assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer")
 
45
  assert mcp.auth.audience == "test-audience"
46
+ assert mcp.auth.required_scopes == ["test-scope1", "test-scope2"]
47
 
48
 
49
  def test_list_of_scopes_must_be_a_list(monkeypatch):
50
+ monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
51
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
52
 
53
  with pytest.raises(ValidationError, match="Input should be a valid list"):
 
55
 
56
 
57
  def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
58
+ monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
59
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
60
 
61
  mcp = FastMCP()
 
64
 
65
 
66
  def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
67
+ monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
68
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
69
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
70
 
 
73
 
74
 
75
  def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
76
+ monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
77
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
78
 
79
  mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))