mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Allow authorization consent screen to be disabled (#2172)
* Add optional authorization consent screen for OAuth providers Adds `require_authorization_consent` parameter (default True) to OAuthProxy and all providers. When disabled, authorization skips the consent screen for local development/testing. Logs security warning when disabled. * Update warning message to use 'authorization consent screen'
This commit is contained in:
parent
422374c7bc
commit
9987a456a0
9 changed files with 108 additions and 12 deletions
|
|
@ -282,6 +282,28 @@ auth = OAuthProxy(
|
|||
|
||||
See [HTTP Deployment - OAuth Token Security](/deployment/http#oauth-token-security).
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="require_authorization_consent" type="bool" default="True">
|
||||
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
|
||||
)
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.
|
||||
</Warning>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
### Using Built-in Providers
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue