fastmcp 2.12.1__py3-none-any.whl → 2.13.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""OIDC Proxy Provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This provider acts as a transparent proxy to an upstream OIDC compliant Authorization
|
|
4
|
+
Server. It leverages the OAuthProxy class to handle Dynamic Client Registration and
|
|
5
|
+
forwarding of all OAuth flows.
|
|
6
|
+
|
|
7
|
+
This implementation is based on:
|
|
8
|
+
OpenID Connect Discovery 1.0 - https://openid.net/specs/openid-connect-discovery-1_0.html
|
|
9
|
+
OAuth 2.0 Authorization Server Metadata - https://datatracker.ietf.org/doc/html/rfc8414
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
16
|
+
from pydantic import AnyHttpUrl, BaseModel, model_validator
|
|
17
|
+
from typing_extensions import Self
|
|
18
|
+
|
|
19
|
+
from fastmcp.server.auth import TokenVerifier
|
|
20
|
+
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
21
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
22
|
+
from fastmcp.utilities.logging import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OIDCConfiguration(BaseModel):
|
|
28
|
+
"""OIDC Configuration.
|
|
29
|
+
|
|
30
|
+
See:
|
|
31
|
+
https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
|
32
|
+
https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
strict: bool = True
|
|
36
|
+
|
|
37
|
+
# OpenID Connect Discovery 1.0
|
|
38
|
+
issuer: AnyHttpUrl | str | None = None # Strict
|
|
39
|
+
|
|
40
|
+
authorization_endpoint: AnyHttpUrl | str | None = None # Strict
|
|
41
|
+
token_endpoint: AnyHttpUrl | str | None = None # Strict
|
|
42
|
+
userinfo_endpoint: AnyHttpUrl | str | None = None
|
|
43
|
+
|
|
44
|
+
jwks_uri: AnyHttpUrl | str | None = None # Strict
|
|
45
|
+
|
|
46
|
+
registration_endpoint: AnyHttpUrl | str | None = None
|
|
47
|
+
|
|
48
|
+
scopes_supported: Sequence[str] | None = None
|
|
49
|
+
|
|
50
|
+
response_types_supported: Sequence[str] | None = None # Strict
|
|
51
|
+
response_modes_supported: Sequence[str] | None = None
|
|
52
|
+
|
|
53
|
+
grant_types_supported: Sequence[str] | None = None
|
|
54
|
+
|
|
55
|
+
acr_values_supported: Sequence[str] | None = None
|
|
56
|
+
|
|
57
|
+
subject_types_supported: Sequence[str] | None = None # Strict
|
|
58
|
+
|
|
59
|
+
id_token_signing_alg_values_supported: Sequence[str] | None = None # Strict
|
|
60
|
+
id_token_encryption_alg_values_supported: Sequence[str] | None = None
|
|
61
|
+
id_token_encryption_enc_values_supported: Sequence[str] | None = None
|
|
62
|
+
|
|
63
|
+
userinfo_signing_alg_values_supported: Sequence[str] | None = None
|
|
64
|
+
userinfo_encryption_alg_values_supported: Sequence[str] | None = None
|
|
65
|
+
userinfo_encryption_enc_values_supported: Sequence[str] | None = None
|
|
66
|
+
|
|
67
|
+
request_object_signing_alg_values_supported: Sequence[str] | None = None
|
|
68
|
+
request_object_encryption_alg_values_supported: Sequence[str] | None = None
|
|
69
|
+
request_object_encryption_enc_values_supported: Sequence[str] | None = None
|
|
70
|
+
|
|
71
|
+
token_endpoint_auth_methods_supported: Sequence[str] | None = None
|
|
72
|
+
token_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = None
|
|
73
|
+
|
|
74
|
+
display_values_supported: Sequence[str] | None = None
|
|
75
|
+
|
|
76
|
+
claim_types_supported: Sequence[str] | None = None
|
|
77
|
+
claims_supported: Sequence[str] | None = None
|
|
78
|
+
|
|
79
|
+
service_documentation: AnyHttpUrl | str | None = None
|
|
80
|
+
|
|
81
|
+
claims_locales_supported: Sequence[str] | None = None
|
|
82
|
+
ui_locales_supported: Sequence[str] | None = None
|
|
83
|
+
|
|
84
|
+
claims_parameter_supported: bool | None = None
|
|
85
|
+
request_parameter_supported: bool | None = None
|
|
86
|
+
request_uri_parameter_supported: bool | None = None
|
|
87
|
+
|
|
88
|
+
require_request_uri_registration: bool | None = None
|
|
89
|
+
|
|
90
|
+
op_policy_uri: AnyHttpUrl | str | None = None
|
|
91
|
+
op_tos_uri: AnyHttpUrl | str | None = None
|
|
92
|
+
|
|
93
|
+
# OAuth 2.0 Authorization Server Metadata
|
|
94
|
+
revocation_endpoint: AnyHttpUrl | str | None = None
|
|
95
|
+
revocation_endpoint_auth_methods_supported: Sequence[str] | None = None
|
|
96
|
+
revocation_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = None
|
|
97
|
+
|
|
98
|
+
introspection_endpoint: AnyHttpUrl | str | None = None
|
|
99
|
+
introspection_endpoint_auth_methods_supported: Sequence[str] | None = None
|
|
100
|
+
introspection_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = (
|
|
101
|
+
None
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
code_challenge_methods_supported: Sequence[str] | None = None
|
|
105
|
+
|
|
106
|
+
signed_metadata: str | None = None
|
|
107
|
+
|
|
108
|
+
@model_validator(mode="after")
|
|
109
|
+
def _enforce_strict(self) -> Self:
|
|
110
|
+
"""Enforce strict rules."""
|
|
111
|
+
if not self.strict:
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def enforce(attr: str, is_url: bool = False) -> None:
|
|
115
|
+
value = getattr(self, attr, None)
|
|
116
|
+
if not value:
|
|
117
|
+
message = f"Missing required configuration metadata: {attr}"
|
|
118
|
+
logger.error(message)
|
|
119
|
+
raise ValueError(message)
|
|
120
|
+
|
|
121
|
+
if not is_url or isinstance(value, AnyHttpUrl):
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
AnyHttpUrl(value)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
message = f"Invalid URL for configuration metadata: {attr}"
|
|
128
|
+
logger.error(message)
|
|
129
|
+
raise ValueError(message) from e
|
|
130
|
+
|
|
131
|
+
enforce("issuer", True)
|
|
132
|
+
enforce("authorization_endpoint", True)
|
|
133
|
+
enforce("token_endpoint", True)
|
|
134
|
+
enforce("jwks_uri", True)
|
|
135
|
+
enforce("response_types_supported")
|
|
136
|
+
enforce("subject_types_supported")
|
|
137
|
+
enforce("id_token_signing_alg_values_supported")
|
|
138
|
+
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def get_oidc_configuration(
|
|
143
|
+
cls, config_url: AnyHttpUrl, *, strict: bool | None, timeout_seconds: int | None
|
|
144
|
+
) -> Self:
|
|
145
|
+
"""Get the OIDC configuration for the specified config URL.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
config_url: The OIDC config URL
|
|
149
|
+
strict: The strict flag for the configuration
|
|
150
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
151
|
+
"""
|
|
152
|
+
get_kwargs = {}
|
|
153
|
+
if timeout_seconds is not None:
|
|
154
|
+
get_kwargs["timeout"] = timeout_seconds
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
response = httpx.get(str(config_url), **get_kwargs)
|
|
158
|
+
response.raise_for_status()
|
|
159
|
+
|
|
160
|
+
config_data = response.json()
|
|
161
|
+
if strict is not None:
|
|
162
|
+
config_data["strict"] = strict
|
|
163
|
+
|
|
164
|
+
return cls.model_validate(config_data)
|
|
165
|
+
except Exception:
|
|
166
|
+
logger.exception(
|
|
167
|
+
f"Unable to get OIDC configuration for config url: {config_url}"
|
|
168
|
+
)
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class OIDCProxy(OAuthProxy):
|
|
173
|
+
"""OAuth provider that wraps OAuthProxy to provide configuration via an OIDC configuration URL.
|
|
174
|
+
|
|
175
|
+
This provider makes it easier to add OAuth protection for any upstream provider
|
|
176
|
+
that is OIDC compliant.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
```python
|
|
180
|
+
from fastmcp import FastMCP
|
|
181
|
+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
182
|
+
|
|
183
|
+
# Simple OIDC based protection
|
|
184
|
+
auth = OIDCProxy(
|
|
185
|
+
config_url="https://oidc.config.url",
|
|
186
|
+
client_id="your-oidc-client-id",
|
|
187
|
+
client_secret="your-oidc-client-secret",
|
|
188
|
+
base_url="https://your.server.url",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
192
|
+
```
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
oidc_config: OIDCConfiguration
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
# OIDC configuration
|
|
201
|
+
config_url: AnyHttpUrl | str,
|
|
202
|
+
strict: bool | None = None,
|
|
203
|
+
# Upstream server configuration
|
|
204
|
+
client_id: str,
|
|
205
|
+
client_secret: str,
|
|
206
|
+
audience: str | None = None,
|
|
207
|
+
timeout_seconds: int | None = None,
|
|
208
|
+
# Token verifier
|
|
209
|
+
token_verifier: TokenVerifier | None = None,
|
|
210
|
+
algorithm: str | None = None,
|
|
211
|
+
required_scopes: list[str] | None = None,
|
|
212
|
+
# FastMCP server configuration
|
|
213
|
+
base_url: AnyHttpUrl | str,
|
|
214
|
+
issuer_url: AnyHttpUrl | str | None = None,
|
|
215
|
+
redirect_path: str | None = None,
|
|
216
|
+
# Client configuration
|
|
217
|
+
allowed_client_redirect_uris: list[str] | None = None,
|
|
218
|
+
client_storage: AsyncKeyValue | None = None,
|
|
219
|
+
# JWT and encryption keys
|
|
220
|
+
jwt_signing_key: str | bytes | None = None,
|
|
221
|
+
# Token validation configuration
|
|
222
|
+
token_endpoint_auth_method: str | None = None,
|
|
223
|
+
# Consent screen configuration
|
|
224
|
+
require_authorization_consent: bool = True,
|
|
225
|
+
consent_csp_policy: str | None = None,
|
|
226
|
+
# Extra parameters
|
|
227
|
+
extra_authorize_params: dict[str, str] | None = None,
|
|
228
|
+
extra_token_params: dict[str, str] | None = None,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Initialize the OIDC proxy provider.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
config_url: URL of upstream configuration
|
|
234
|
+
strict: Optional strict flag for the configuration
|
|
235
|
+
client_id: Client ID registered with upstream server
|
|
236
|
+
client_secret: Client secret for upstream server
|
|
237
|
+
audience: Audience for upstream server
|
|
238
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
239
|
+
token_verifier: Optional custom token verifier (e.g., IntrospectionTokenVerifier for opaque tokens).
|
|
240
|
+
If not provided, a JWTVerifier will be created using the OIDC configuration.
|
|
241
|
+
Cannot be used with algorithm or required_scopes parameters (configure these on your verifier instead).
|
|
242
|
+
algorithm: Token verifier algorithm (only used if token_verifier is not provided)
|
|
243
|
+
required_scopes: Required scopes for token validation (only used if token_verifier is not provided)
|
|
244
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
245
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
246
|
+
to avoid 404s during discovery when mounting under a path.
|
|
247
|
+
redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
|
|
248
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
249
|
+
Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
|
|
250
|
+
If None (default), only localhost redirect URIs are allowed.
|
|
251
|
+
If empty list, all redirect URIs are allowed (not recommended for production).
|
|
252
|
+
These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
|
|
253
|
+
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
254
|
+
If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
|
|
255
|
+
disk store will be encrypted using a key derived from the JWT Signing Key.
|
|
256
|
+
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
|
|
257
|
+
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
|
|
258
|
+
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
|
|
259
|
+
token_endpoint_auth_method: Token endpoint authentication method for upstream server.
|
|
260
|
+
Common values: "client_secret_basic", "client_secret_post", "none".
|
|
261
|
+
If None, authlib will use its default (typically "client_secret_basic").
|
|
262
|
+
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
|
263
|
+
When True, users see a consent screen before being redirected to the upstream IdP.
|
|
264
|
+
When False, authorization proceeds directly without user confirmation.
|
|
265
|
+
SECURITY WARNING: Only disable for local development or testing environments.
|
|
266
|
+
consent_csp_policy: Content Security Policy for the consent page.
|
|
267
|
+
If None (default), uses the built-in CSP policy with appropriate directives.
|
|
268
|
+
If empty string "", disables CSP entirely (no meta tag is rendered).
|
|
269
|
+
If a non-empty string, uses that as the CSP policy value.
|
|
270
|
+
extra_authorize_params: Additional parameters to forward to the upstream authorization endpoint.
|
|
271
|
+
Useful for provider-specific parameters like prompt=consent or access_type=offline.
|
|
272
|
+
Example: {"prompt": "consent", "access_type": "offline"}
|
|
273
|
+
extra_token_params: Additional parameters to forward to the upstream token endpoint.
|
|
274
|
+
Useful for provider-specific parameters during token exchange.
|
|
275
|
+
"""
|
|
276
|
+
if not config_url:
|
|
277
|
+
raise ValueError("Missing required config URL")
|
|
278
|
+
|
|
279
|
+
if not client_id:
|
|
280
|
+
raise ValueError("Missing required client id")
|
|
281
|
+
|
|
282
|
+
if not client_secret:
|
|
283
|
+
raise ValueError("Missing required client secret")
|
|
284
|
+
|
|
285
|
+
if not base_url:
|
|
286
|
+
raise ValueError("Missing required base URL")
|
|
287
|
+
|
|
288
|
+
# Validate that verifier-specific parameters are not used with custom verifier
|
|
289
|
+
if token_verifier is not None:
|
|
290
|
+
if algorithm is not None:
|
|
291
|
+
raise ValueError(
|
|
292
|
+
"Cannot specify 'algorithm' when providing a custom token_verifier. "
|
|
293
|
+
"Configure the algorithm on your token verifier instead."
|
|
294
|
+
)
|
|
295
|
+
if required_scopes is not None:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
"Cannot specify 'required_scopes' when providing a custom token_verifier. "
|
|
298
|
+
"Configure required scopes on your token verifier instead."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if isinstance(config_url, str):
|
|
302
|
+
config_url = AnyHttpUrl(config_url)
|
|
303
|
+
|
|
304
|
+
self.oidc_config = self.get_oidc_configuration(
|
|
305
|
+
config_url, strict, timeout_seconds
|
|
306
|
+
)
|
|
307
|
+
if (
|
|
308
|
+
not self.oidc_config.authorization_endpoint
|
|
309
|
+
or not self.oidc_config.token_endpoint
|
|
310
|
+
):
|
|
311
|
+
logger.debug(f"Invalid OIDC Configuration: {self.oidc_config}")
|
|
312
|
+
raise ValueError("Missing required OIDC endpoints")
|
|
313
|
+
|
|
314
|
+
revocation_endpoint = (
|
|
315
|
+
str(self.oidc_config.revocation_endpoint)
|
|
316
|
+
if self.oidc_config.revocation_endpoint
|
|
317
|
+
else None
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Use custom verifier if provided, otherwise create default JWTVerifier
|
|
321
|
+
if token_verifier is None:
|
|
322
|
+
token_verifier = self.get_token_verifier(
|
|
323
|
+
algorithm=algorithm,
|
|
324
|
+
audience=audience,
|
|
325
|
+
required_scopes=required_scopes,
|
|
326
|
+
timeout_seconds=timeout_seconds,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
init_kwargs = {
|
|
330
|
+
"upstream_authorization_endpoint": str(
|
|
331
|
+
self.oidc_config.authorization_endpoint
|
|
332
|
+
),
|
|
333
|
+
"upstream_token_endpoint": str(self.oidc_config.token_endpoint),
|
|
334
|
+
"upstream_client_id": client_id,
|
|
335
|
+
"upstream_client_secret": client_secret,
|
|
336
|
+
"upstream_revocation_endpoint": revocation_endpoint,
|
|
337
|
+
"token_verifier": token_verifier,
|
|
338
|
+
"base_url": base_url,
|
|
339
|
+
"issuer_url": issuer_url or base_url,
|
|
340
|
+
"service_documentation_url": self.oidc_config.service_documentation,
|
|
341
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
342
|
+
"client_storage": client_storage,
|
|
343
|
+
"jwt_signing_key": jwt_signing_key,
|
|
344
|
+
"token_endpoint_auth_method": token_endpoint_auth_method,
|
|
345
|
+
"require_authorization_consent": require_authorization_consent,
|
|
346
|
+
"consent_csp_policy": consent_csp_policy,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if redirect_path:
|
|
350
|
+
init_kwargs["redirect_path"] = redirect_path
|
|
351
|
+
|
|
352
|
+
# Build extra params, merging audience with user-provided params
|
|
353
|
+
# User params override audience if there's a conflict
|
|
354
|
+
final_authorize_params: dict[str, str] = {}
|
|
355
|
+
final_token_params: dict[str, str] = {}
|
|
356
|
+
|
|
357
|
+
if audience:
|
|
358
|
+
final_authorize_params["audience"] = audience
|
|
359
|
+
final_token_params["audience"] = audience
|
|
360
|
+
|
|
361
|
+
if extra_authorize_params:
|
|
362
|
+
final_authorize_params.update(extra_authorize_params)
|
|
363
|
+
if extra_token_params:
|
|
364
|
+
final_token_params.update(extra_token_params)
|
|
365
|
+
|
|
366
|
+
if final_authorize_params:
|
|
367
|
+
init_kwargs["extra_authorize_params"] = final_authorize_params
|
|
368
|
+
if final_token_params:
|
|
369
|
+
init_kwargs["extra_token_params"] = final_token_params
|
|
370
|
+
|
|
371
|
+
super().__init__(**init_kwargs) # ty: ignore[invalid-argument-type]
|
|
372
|
+
|
|
373
|
+
def get_oidc_configuration(
|
|
374
|
+
self,
|
|
375
|
+
config_url: AnyHttpUrl,
|
|
376
|
+
strict: bool | None,
|
|
377
|
+
timeout_seconds: int | None,
|
|
378
|
+
) -> OIDCConfiguration:
|
|
379
|
+
"""Gets the OIDC configuration for the specified configuration URL.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
config_url: The OIDC configuration URL
|
|
383
|
+
strict: The strict flag for the configuration
|
|
384
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
385
|
+
"""
|
|
386
|
+
return OIDCConfiguration.get_oidc_configuration(
|
|
387
|
+
config_url, strict=strict, timeout_seconds=timeout_seconds
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def get_token_verifier(
|
|
391
|
+
self,
|
|
392
|
+
*,
|
|
393
|
+
algorithm: str | None = None,
|
|
394
|
+
audience: str | None = None,
|
|
395
|
+
required_scopes: list[str] | None = None,
|
|
396
|
+
timeout_seconds: int | None = None,
|
|
397
|
+
) -> TokenVerifier:
|
|
398
|
+
"""Creates the token verifier for the specified OIDC configuration and arguments.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
algorithm: Optional token verifier algorithm
|
|
402
|
+
audience: Optional token verifier audience
|
|
403
|
+
required_scopes: Optional token verifier required_scopes
|
|
404
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
405
|
+
"""
|
|
406
|
+
return JWTVerifier(
|
|
407
|
+
jwks_uri=str(self.oidc_config.jwks_uri),
|
|
408
|
+
issuer=str(self.oidc_config.issuer),
|
|
409
|
+
algorithm=algorithm,
|
|
410
|
+
audience=audience,
|
|
411
|
+
required_scopes=required_scopes,
|
|
412
|
+
)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Auth0 OAuth provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides a complete Auth0 integration that's ready to use with
|
|
4
|
+
just the configuration URL, client ID, client secret, audience, and base URL.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from fastmcp.server.auth.providers.auth0 import Auth0Provider
|
|
10
|
+
|
|
11
|
+
# Simple Auth0 OAuth protection
|
|
12
|
+
auth = Auth0Provider(
|
|
13
|
+
config_url="https://auth0.config.url",
|
|
14
|
+
client_id="your-auth0-client-id",
|
|
15
|
+
client_secret="your-auth0-client-secret",
|
|
16
|
+
audience="your-auth0-api-audience",
|
|
17
|
+
base_url="http://localhost:8000",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
21
|
+
```
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
25
|
+
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
26
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
27
|
+
|
|
28
|
+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
29
|
+
from fastmcp.settings import ENV_FILE
|
|
30
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
31
|
+
from fastmcp.utilities.logging import get_logger
|
|
32
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Auth0ProviderSettings(BaseSettings):
|
|
38
|
+
"""Settings for Auth0 OIDC provider."""
|
|
39
|
+
|
|
40
|
+
model_config = SettingsConfigDict(
|
|
41
|
+
env_prefix="FASTMCP_SERVER_AUTH_AUTH0_",
|
|
42
|
+
env_file=ENV_FILE,
|
|
43
|
+
extra="ignore",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
config_url: AnyHttpUrl | None = None
|
|
47
|
+
client_id: str | None = None
|
|
48
|
+
client_secret: SecretStr | None = None
|
|
49
|
+
audience: str | None = None
|
|
50
|
+
base_url: AnyHttpUrl | None = None
|
|
51
|
+
issuer_url: AnyHttpUrl | None = None
|
|
52
|
+
redirect_path: str | None = None
|
|
53
|
+
required_scopes: list[str] | None = None
|
|
54
|
+
allowed_client_redirect_uris: list[str] | None = None
|
|
55
|
+
jwt_signing_key: str | None = None
|
|
56
|
+
|
|
57
|
+
@field_validator("required_scopes", mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def _parse_scopes(cls, v):
|
|
60
|
+
return parse_scopes(v)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Auth0Provider(OIDCProxy):
|
|
64
|
+
"""An Auth0 provider implementation for FastMCP.
|
|
65
|
+
|
|
66
|
+
This provider is a complete Auth0 integration that's ready to use with
|
|
67
|
+
just the configuration URL, client ID, client secret, audience, and base URL.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
```python
|
|
71
|
+
from fastmcp import FastMCP
|
|
72
|
+
from fastmcp.server.auth.providers.auth0 import Auth0Provider
|
|
73
|
+
|
|
74
|
+
# Simple Auth0 OAuth protection
|
|
75
|
+
auth = Auth0Provider(
|
|
76
|
+
config_url="https://auth0.config.url",
|
|
77
|
+
client_id="your-auth0-client-id",
|
|
78
|
+
client_secret="your-auth0-client-secret",
|
|
79
|
+
audience="your-auth0-api-audience",
|
|
80
|
+
base_url="http://localhost:8000",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
config_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
91
|
+
client_id: str | NotSetT = NotSet,
|
|
92
|
+
client_secret: str | NotSetT = NotSet,
|
|
93
|
+
audience: str | NotSetT = NotSet,
|
|
94
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
95
|
+
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
96
|
+
required_scopes: list[str] | NotSetT = NotSet,
|
|
97
|
+
redirect_path: str | NotSetT = NotSet,
|
|
98
|
+
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
99
|
+
client_storage: AsyncKeyValue | None = None,
|
|
100
|
+
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
|
101
|
+
require_authorization_consent: bool = True,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Initialize Auth0 OAuth provider.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config_url: Auth0 config URL
|
|
107
|
+
client_id: Auth0 application client id
|
|
108
|
+
client_secret: Auth0 application client secret
|
|
109
|
+
audience: Auth0 API audience
|
|
110
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
111
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
112
|
+
to avoid 404s during discovery when mounting under a path.
|
|
113
|
+
required_scopes: Required Auth0 scopes (defaults to ["openid"])
|
|
114
|
+
redirect_path: Redirect path configured in Auth0 application
|
|
115
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
116
|
+
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
117
|
+
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
118
|
+
If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
|
|
119
|
+
disk store will be encrypted using a key derived from the JWT Signing Key.
|
|
120
|
+
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
|
|
121
|
+
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
|
|
122
|
+
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
|
|
123
|
+
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
|
124
|
+
When True, users see a consent screen before being redirected to Auth0.
|
|
125
|
+
When False, authorization proceeds directly without user confirmation.
|
|
126
|
+
SECURITY WARNING: Only disable for local development or testing environments.
|
|
127
|
+
"""
|
|
128
|
+
settings = Auth0ProviderSettings.model_validate(
|
|
129
|
+
{
|
|
130
|
+
k: v
|
|
131
|
+
for k, v in {
|
|
132
|
+
"config_url": config_url,
|
|
133
|
+
"client_id": client_id,
|
|
134
|
+
"client_secret": client_secret,
|
|
135
|
+
"audience": audience,
|
|
136
|
+
"base_url": base_url,
|
|
137
|
+
"issuer_url": issuer_url,
|
|
138
|
+
"required_scopes": required_scopes,
|
|
139
|
+
"redirect_path": redirect_path,
|
|
140
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
141
|
+
"jwt_signing_key": jwt_signing_key,
|
|
142
|
+
}.items()
|
|
143
|
+
if v is not NotSet
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if not settings.config_url:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
"config_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not settings.client_id:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if not settings.client_secret:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if not settings.audience:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"audience is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if not settings.base_url:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_AUTH0_BASE_URL"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
auth0_required_scopes = settings.required_scopes or ["openid"]
|
|
173
|
+
|
|
174
|
+
super().__init__(
|
|
175
|
+
config_url=settings.config_url,
|
|
176
|
+
client_id=settings.client_id,
|
|
177
|
+
client_secret=settings.client_secret.get_secret_value(),
|
|
178
|
+
audience=settings.audience,
|
|
179
|
+
base_url=settings.base_url,
|
|
180
|
+
issuer_url=settings.issuer_url,
|
|
181
|
+
redirect_path=settings.redirect_path,
|
|
182
|
+
required_scopes=auth0_required_scopes,
|
|
183
|
+
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
|
|
184
|
+
client_storage=client_storage,
|
|
185
|
+
jwt_signing_key=settings.jwt_signing_key,
|
|
186
|
+
require_authorization_consent=require_authorization_consent,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
logger.debug(
|
|
190
|
+
"Initialized Auth0 OAuth provider for client %s with scopes: %s",
|
|
191
|
+
settings.client_id,
|
|
192
|
+
auth0_required_scopes,
|
|
193
|
+
)
|