diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx index 21ad2b917..e7507c303 100644 --- a/docs/servers/auth/oauth-proxy.mdx +++ b/docs/servers/auth/oauth-proxy.mdx @@ -282,6 +282,28 @@ auth = OAuthProxy( See [HTTP Deployment - OAuth Token Security](/deployment/http#oauth-token-security). + + + Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access, preventing [confused deputy attacks](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) by ensuring users explicitly approve new clients. + + **Default behavior (True):** + Users see a consent screen on first authorization. Consent choices are remembered via signed cookies, so users only need to approve each client once. This protects against malicious clients impersonating the user. + + **Disabling consent (False):** + Authorization proceeds directly to the upstream provider without user confirmation. Only use this for local development or testing environments where the security trade-off is acceptable. + + ```python + # Development/testing only - skip consent screen + auth = OAuthProxy( + ..., + require_authorization_consent=False # ⚠️ Security warning: only for local/testing + ) + ``` + + + Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients. + + ### Using Built-in Providers diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py index c525da1af..c2bc7d339 100644 --- a/src/fastmcp/server/auth/oauth_proxy.py +++ b/src/fastmcp/server/auth/oauth_proxy.py @@ -562,6 +562,8 @@ class OAuthProxy(OAuthProvider): jwt_signing_key: str | bytes | None = None, # Token encryption key (optional, ephemeral if not provided) token_encryption_key: str | bytes | None = None, + # Consent screen configuration + require_authorization_consent: bool = True, ): """Initialize the OAuth proxy provider. @@ -602,6 +604,10 @@ class OAuthProxy(OAuthProvider): token_encryption_key: Optional secret for encrypting upstream tokens at rest (accepts any string or bytes). Default: ephemeral (random salt at startup, won't survive restart). Production: provide explicit key from environment variable. + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to the upstream IdP. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ # Always enable DCR since we implement it locally for MCP clients client_registration_options = ClientRegistrationOptions( @@ -655,6 +661,14 @@ class OAuthProxy(OAuthProvider): # Token endpoint authentication self._token_endpoint_auth_method = token_endpoint_auth_method + # Consent screen configuration + self._require_authorization_consent = require_authorization_consent + if not require_authorization_consent: + logger.warning( + "Authorization consent screen disabled - only use for local development or testing. " + "In production, this screen protects against confused deputy attacks." + ) + # Extra parameters for authorization and token endpoints self._extra_authorize_params = extra_authorize_params or {} self._extra_token_params = extra_token_params or {} @@ -915,6 +929,9 @@ class OAuthProxy(OAuthProvider): 1. Store transaction with client details and PKCE (if forwarding) 2. Return local /consent URL; browser visits consent first 3. Consent handler redirects to upstream IdP if approved/already approved + + If consent is disabled (require_authorization_consent=False), skip the consent screen + and redirect directly to the upstream IdP. """ # Generate transaction ID for this authorization request txn_id = secrets.token_urlsafe(32) @@ -930,23 +947,37 @@ class OAuthProxy(OAuthProvider): ) # Store transaction data for IdP callback processing + transaction = OAuthTransaction( + txn_id=txn_id, + client_id=client.client_id, + client_redirect_uri=str(params.redirect_uri), + client_state=params.state or "", + code_challenge=params.code_challenge, + code_challenge_method=getattr(params, "code_challenge_method", "S256"), + scopes=params.scopes or [], + created_at=time.time(), + resource=getattr(params, "resource", None), + proxy_code_verifier=proxy_code_verifier, + ) await self._transaction_store.put( key=txn_id, - value=OAuthTransaction( - txn_id=txn_id, - client_id=client.client_id, - client_redirect_uri=str(params.redirect_uri), - client_state=params.state or "", - code_challenge=params.code_challenge, - code_challenge_method=getattr(params, "code_challenge_method", "S256"), - scopes=params.scopes or [], - created_at=time.time(), - resource=getattr(params, "resource", None), - proxy_code_verifier=proxy_code_verifier, - ), + value=transaction, ttl=15 * 60, # Auto-expire after 15 minutes ) + # If consent is disabled, skip consent screen and go directly to upstream IdP + if not self._require_authorization_consent: + upstream_url = self._build_upstream_authorize_url( + txn_id, transaction.model_dump() + ) + logger.debug( + "Starting OAuth transaction %s for client %s, redirecting directly to upstream IdP (consent disabled, PKCE forwarding: %s)", + txn_id, + client.client_id, + "enabled" if proxy_code_challenge else "disabled", + ) + return upstream_url + consent_url = f"{str(self.base_url).rstrip('/')}/consent?txn_id={txn_id}" logger.debug( diff --git a/src/fastmcp/server/auth/oidc_proxy.py b/src/fastmcp/server/auth/oidc_proxy.py index 6d5c82fe8..b7fd7e72b 100644 --- a/src/fastmcp/server/auth/oidc_proxy.py +++ b/src/fastmcp/server/auth/oidc_proxy.py @@ -217,6 +217,8 @@ class OIDCProxy(OAuthProxy): client_storage: AsyncKeyValue | None = None, # Token validation configuration token_endpoint_auth_method: str | None = None, + # Consent screen configuration + require_authorization_consent: bool = True, ) -> None: """Initialize the OIDC proxy provider. @@ -242,6 +244,10 @@ class OIDCProxy(OAuthProxy): token_endpoint_auth_method: Token endpoint authentication method for upstream server. Common values: "client_secret_basic", "client_secret_post", "none". If None, authlib will use its default (typically "client_secret_basic"). + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to the upstream IdP. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ if not config_url: raise ValueError("Missing required config URL") @@ -296,6 +302,7 @@ class OIDCProxy(OAuthProxy): "allowed_client_redirect_uris": allowed_client_redirect_uris, "client_storage": client_storage, "token_endpoint_auth_method": token_endpoint_auth_method, + "require_authorization_consent": require_authorization_consent, } if redirect_path: diff --git a/src/fastmcp/server/auth/providers/auth0.py b/src/fastmcp/server/auth/providers/auth0.py index ce200efa5..04f625eb5 100644 --- a/src/fastmcp/server/auth/providers/auth0.py +++ b/src/fastmcp/server/auth/providers/auth0.py @@ -96,6 +96,7 @@ class Auth0Provider(OIDCProxy): redirect_path: str | NotSetT = NotSet, allowed_client_redirect_uris: list[str] | NotSetT = NotSet, client_storage: AsyncKeyValue | None = None, + require_authorization_consent: bool = True, ) -> None: """Initialize Auth0 OAuth provider. @@ -112,6 +113,10 @@ class Auth0Provider(OIDCProxy): allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. 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 + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to Auth0. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ settings = Auth0ProviderSettings.model_validate( { @@ -169,6 +174,7 @@ class Auth0Provider(OIDCProxy): "required_scopes": auth0_required_scopes, "allowed_client_redirect_uris": settings.allowed_client_redirect_uris, "client_storage": client_storage, + "require_authorization_consent": require_authorization_consent, } super().__init__(**init_kwargs) diff --git a/src/fastmcp/server/auth/providers/aws.py b/src/fastmcp/server/auth/providers/aws.py index 695037f7d..b3ff650c3 100644 --- a/src/fastmcp/server/auth/providers/aws.py +++ b/src/fastmcp/server/auth/providers/aws.py @@ -135,6 +135,7 @@ class AWSCognitoProvider(OIDCProxy): required_scopes: list[str] | NotSetT = NotSet, allowed_client_redirect_uris: list[str] | NotSetT = NotSet, client_storage: AsyncKeyValue | None = None, + require_authorization_consent: bool = True, ): """Initialize AWS Cognito OAuth provider. @@ -151,6 +152,10 @@ class AWSCognitoProvider(OIDCProxy): allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. 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 + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to AWS Cognito. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ settings = AWSCognitoProviderSettings.model_validate( @@ -215,6 +220,7 @@ class AWSCognitoProvider(OIDCProxy): redirect_path=redirect_path_final, allowed_client_redirect_uris=allowed_client_redirect_uris_final, client_storage=client_storage, + require_authorization_consent=require_authorization_consent, ) logger.debug( diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py index 93ad94182..4b429bf64 100644 --- a/src/fastmcp/server/auth/providers/azure.py +++ b/src/fastmcp/server/auth/providers/azure.py @@ -109,6 +109,7 @@ class AzureProvider(OAuthProxy): additional_authorize_scopes: list[str] | None | NotSetT = NotSet, allowed_client_redirect_uris: list[str] | NotSetT = NotSet, client_storage: AsyncKeyValue | None = None, + require_authorization_consent: bool = True, ) -> None: """Initialize Azure OAuth provider. @@ -131,6 +132,10 @@ class AzureProvider(OAuthProxy): allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. 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 + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to Azure. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ settings = AzureProviderSettings.model_validate( { @@ -216,6 +221,7 @@ class AzureProvider(OAuthProxy): or settings.base_url, # Default to base_url if not specified allowed_client_redirect_uris=settings.allowed_client_redirect_uris, client_storage=client_storage, + require_authorization_consent=require_authorization_consent, ) logger.info( diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py index 9526174fc..7db1bc7ee 100644 --- a/src/fastmcp/server/auth/providers/github.py +++ b/src/fastmcp/server/auth/providers/github.py @@ -206,6 +206,7 @@ class GitHubProvider(OAuthProxy): timeout_seconds: int | NotSetT = NotSet, allowed_client_redirect_uris: list[str] | NotSetT = NotSet, client_storage: AsyncKeyValue | None = None, + require_authorization_consent: bool = True, ): """Initialize GitHub OAuth provider. @@ -221,6 +222,10 @@ class GitHubProvider(OAuthProxy): allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. 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 + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to GitHub. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ settings = GitHubProviderSettings.model_validate( @@ -280,6 +285,7 @@ class GitHubProvider(OAuthProxy): or settings.base_url, # Default to base_url if not specified allowed_client_redirect_uris=allowed_client_redirect_uris_final, client_storage=client_storage, + require_authorization_consent=require_authorization_consent, ) logger.debug( diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py index eb7f631be..814f9b1e8 100644 --- a/src/fastmcp/server/auth/providers/google.py +++ b/src/fastmcp/server/auth/providers/google.py @@ -222,6 +222,7 @@ class GoogleProvider(OAuthProxy): timeout_seconds: int | NotSetT = NotSet, allowed_client_redirect_uris: list[str] | NotSetT = NotSet, client_storage: AsyncKeyValue | None = None, + require_authorization_consent: bool = True, ): """Initialize Google OAuth provider. @@ -240,6 +241,10 @@ class GoogleProvider(OAuthProxy): allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. 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 + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to Google. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ settings = GoogleProviderSettings.model_validate( @@ -299,6 +304,7 @@ class GoogleProvider(OAuthProxy): or settings.base_url, # Default to base_url if not specified allowed_client_redirect_uris=allowed_client_redirect_uris_final, client_storage=client_storage, + require_authorization_consent=require_authorization_consent, ) logger.debug( diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py index 2863f0d27..d5d7081eb 100644 --- a/src/fastmcp/server/auth/providers/workos.py +++ b/src/fastmcp/server/auth/providers/workos.py @@ -172,6 +172,7 @@ class WorkOSProvider(OAuthProxy): timeout_seconds: int | NotSetT = NotSet, allowed_client_redirect_uris: list[str] | NotSetT = NotSet, client_storage: AsyncKeyValue | None = None, + require_authorization_consent: bool = True, ): """Initialize WorkOS OAuth provider. @@ -188,6 +189,10 @@ class WorkOSProvider(OAuthProxy): allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. 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 + require_authorization_consent: Whether to require user consent before authorizing clients (default True). + When True, users see a consent screen before being redirected to WorkOS. + When False, authorization proceeds directly without user confirmation. + SECURITY WARNING: Only disable for local development or testing environments. """ settings = WorkOSProviderSettings.model_validate( @@ -256,6 +261,7 @@ class WorkOSProvider(OAuthProxy): or settings.base_url, # Default to base_url if not specified allowed_client_redirect_uris=allowed_client_redirect_uris_final, client_storage=client_storage, + require_authorization_consent=require_authorization_consent, ) logger.debug(