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:
Jeremiah Lowin 2025-10-21 10:00:39 -04:00 committed by GitHub
commit 9987a456a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 108 additions and 12 deletions

View file

@ -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

View file

@ -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(

View file

@ -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:

View file

@ -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)

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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(