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:
Yair Weinberger
2025-10-24 15:43:29 +03:00
parent 95d5c8b4ad
commit 241f0987ae
4 changed files with 199 additions and 10 deletions

View File

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