feat: add external OAuth 2.1 provider mode for bearer token authentication
Add support for external OAuth 2.1 provider mode where authentication is handled by external systems that issue Google OAuth access tokens. **Changes:** 1. **New Environment Variable: `EXTERNAL_OAUTH21_PROVIDER`** - Enables external OAuth mode when set to `true` - Requires `MCP_ENABLE_OAUTH21=true` - Disables protocol-level auth (MCP handshake/tools list work without auth) - Requires bearer tokens in Authorization headers for tool calls 2. **New File: `auth/external_oauth_provider.py`** - Custom provider extending FastMCP's GoogleProvider - Handles ya29.* Google OAuth access tokens - Validates tokens via google-auth library + userinfo API - Returns properly formatted AccessToken objects 3. **Modified: `auth/oauth_config.py`** - Add `external_oauth21_provider` config option - Validation that external mode requires OAuth 2.1 - Helper methods for checking external provider mode 4. **Modified: `core/server.py`** - Use ExternalOAuthProvider when external mode enabled - Use standard GoogleProvider otherwise - Set server.auth = None for external mode (no protocol auth) 5. **Modified: `README.md`** - New "External OAuth 2.1 Provider Mode" section - Usage examples and configuration - Added to environment variables table **How It Works:** - MCP handshake and tools/list do NOT require authentication - Tool calls require `Authorization: Bearer ya29.xxx` headers - Tokens validated by calling Google's userinfo API - Multi-user support via per-request authentication - Stateless-compatible for containerized deployments **Use Cases:** - Integrating with existing authentication systems - Custom OAuth flows managed by your application - API gateways handling authentication upstream - Multi-tenant SaaS with centralized auth - Mobile/web apps with their own OAuth implementation **Example Configuration:** ```bash export MCP_ENABLE_OAUTH21=true export EXTERNAL_OAUTH21_PROVIDER=true export GOOGLE_OAUTH_CLIENT_ID=your_client_id export GOOGLE_OAUTH_CLIENT_SECRET=your_client_secret export WORKSPACE_MCP_STATELESS_MODE=true # Optional ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,11 @@ class OAuthConfig:
|
||||
self.pkce_required = self.oauth21_enabled # PKCE is mandatory in OAuth 2.1
|
||||
self.supported_code_challenge_methods = ["S256", "plain"] if not self.oauth21_enabled else ["S256"]
|
||||
|
||||
# External OAuth 2.1 provider configuration
|
||||
self.external_oauth21_provider = os.getenv("EXTERNAL_OAUTH21_PROVIDER", "false").lower() == "true"
|
||||
if self.external_oauth21_provider and not self.oauth21_enabled:
|
||||
raise ValueError("EXTERNAL_OAUTH21_PROVIDER requires MCP_ENABLE_OAUTH21=true")
|
||||
|
||||
# Stateless mode configuration
|
||||
self.stateless_mode = os.getenv("WORKSPACE_MCP_STATELESS_MODE", "false").lower() == "true"
|
||||
if self.stateless_mode and not self.oauth21_enabled:
|
||||
@@ -87,7 +92,11 @@ class OAuthConfig:
|
||||
if value and key not in os.environ:
|
||||
os.environ[key] = value
|
||||
|
||||
_set_if_absent("FASTMCP_SERVER_AUTH", "fastmcp.server.auth.providers.google.GoogleProvider" if self.oauth21_enabled else None)
|
||||
# Don't set FASTMCP_SERVER_AUTH if using external OAuth provider
|
||||
# (external OAuth means protocol-level auth is disabled, only tool-level auth)
|
||||
if not self.external_oauth21_provider:
|
||||
_set_if_absent("FASTMCP_SERVER_AUTH", "fastmcp.server.auth.providers.google.GoogleProvider" if self.oauth21_enabled else None)
|
||||
|
||||
_set_if_absent("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID", self.client_id)
|
||||
_set_if_absent("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET", self.client_secret)
|
||||
_set_if_absent("FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL", self.get_oauth_base_url())
|
||||
@@ -190,6 +199,7 @@ class OAuthConfig:
|
||||
"redirect_path": self.redirect_path,
|
||||
"client_configured": bool(self.client_id),
|
||||
"oauth21_enabled": self.oauth21_enabled,
|
||||
"external_oauth21_provider": self.external_oauth21_provider,
|
||||
"pkce_required": self.pkce_required,
|
||||
"transport_mode": self._transport_mode,
|
||||
"total_redirect_uris": len(self.get_redirect_uris()),
|
||||
@@ -223,6 +233,18 @@ class OAuthConfig:
|
||||
"""
|
||||
return self.oauth21_enabled
|
||||
|
||||
def is_external_oauth21_provider(self) -> bool:
|
||||
"""
|
||||
Check if external OAuth 2.1 provider mode is enabled.
|
||||
|
||||
When enabled, the server expects external OAuth flow with bearer tokens
|
||||
in Authorization headers for tool calls. Protocol-level auth is disabled.
|
||||
|
||||
Returns:
|
||||
True if external OAuth 2.1 provider is enabled
|
||||
"""
|
||||
return self.external_oauth21_provider
|
||||
|
||||
def detect_oauth_version(self, request_params: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Detect OAuth version based on request parameters.
|
||||
@@ -383,3 +405,8 @@ def get_oauth_redirect_uri() -> str:
|
||||
def is_stateless_mode() -> bool:
|
||||
"""Check if stateless mode is enabled."""
|
||||
return get_oauth_config().stateless_mode
|
||||
|
||||
|
||||
def is_external_oauth21_provider() -> bool:
|
||||
"""Check if external OAuth 2.1 provider mode is enabled."""
|
||||
return get_oauth_config().is_external_oauth21_provider()
|
||||
|
||||
Reference in New Issue
Block a user