Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion auth/external_oauth_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,54 @@

Extends FastMCP's GoogleProvider to support external OAuth flows where
access tokens (ya29.*) are issued by external systems and need validation.

This provider acts as a Resource Server only - it validates tokens issued by
Google's Authorization Server but does not issue tokens itself.
"""

import logging
import time
from typing import Optional

from starlette.routing import Route
from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.auth import AccessToken
from google.oauth2.credentials import Credentials

logger = logging.getLogger(__name__)

# Google's OAuth 2.0 Authorization Server
GOOGLE_ISSUER_URL = "https://accounts.google.com"


class ExternalOAuthProvider(GoogleProvider):
"""
Extended GoogleProvider that supports validating external Google OAuth access tokens.

This provider handles ya29.* access tokens by calling Google's userinfo API,
while maintaining compatibility with standard JWT ID tokens.

Unlike the standard GoogleProvider, this acts as a Resource Server only:
- Does NOT create /authorize, /token, /register endpoints
- Only advertises Google's authorization server in metadata
- Only validates tokens, does not issue them
"""

def __init__(self, client_id: str, client_secret: str, **kwargs):
def __init__(
self,
client_id: str,
client_secret: str,
resource_server_url: Optional[str] = None,
**kwargs,
):
"""Initialize and store client credentials for token validation."""
self._resource_server_url = resource_server_url
super().__init__(client_id=client_id, client_secret=client_secret, **kwargs)
# Store credentials as they're not exposed by parent class
self._client_id = client_id
self._client_secret = client_secret
# Store as string - Pydantic validates it when passed to models

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment mentions Pydantic validation but the assignment is a simple string copy without any validation happening at this point. Consider removing the misleading comment or adding actual validation if needed.

Suggested change
# Store as string - Pydantic validates it when passed to models
# Store as string for compatibility with any Pydantic models that may consume it

Copilot uses AI. Check for mistakes.
self.resource_server_url = self._resource_server_url

async def verify_token(self, token: str) -> Optional[AccessToken]:
"""
Expand Down Expand Up @@ -97,3 +118,40 @@ async def verify_token(self, token: str) -> Optional[AccessToken]:

# For JWT tokens, use parent class implementation
return await super().verify_token(token)

def get_routes(self, **kwargs) -> list[Route]:
"""
Get OAuth routes for external provider mode.

Returns only protected resource metadata routes that point to Google
as the authorization server. Does not create authorization server routes
(/authorize, /token, etc.) since tokens are issued by Google directly.

Args:
**kwargs: Additional arguments passed by FastMCP (e.g., mcp_path)

Returns:
List of routes - only protected resource metadata
"""
from mcp.server.auth.routes import create_protected_resource_routes

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import statements should be at the module level rather than inside a method. Move this import to the top of the file with other imports for better code organization and to avoid repeated import overhead on each method call.

Copilot uses AI. Check for mistakes.

if not self.resource_server_url:
logger.warning(
"ExternalOAuthProvider: resource_server_url not set, no routes created"
)
return []

# Create protected resource routes that point to Google as the authorization server
# Pass strings directly - Pydantic validates them during model construction
protected_routes = create_protected_resource_routes(
resource_url=self.resource_server_url,
authorization_servers=[GOOGLE_ISSUER_URL],
scopes_supported=self.required_scopes,
resource_name="Google Workspace MCP",
resource_documentation=None,
)

logger.info(
f"ExternalOAuthProvider: Created protected resource routes pointing to {GOOGLE_ISSUER_URL}"
)
return protected_routes
10 changes: 6 additions & 4 deletions core/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,14 +359,16 @@ def validate_and_derive_jwt_key(
base_url=config.get_oauth_base_url(),
redirect_path=config.redirect_path,
required_scopes=required_scopes,
resource_server_url=config.get_oauth_base_url(),
)
# Disable protocol-level auth, expect bearer tokens in tool calls
server.auth = None
server.auth = provider

logger.info("OAuth 2.1 enabled with EXTERNAL provider mode")
logger.info(
"OAuth 2.1 enabled with EXTERNAL provider mode - protocol-level auth disabled"
"Expecting Authorization bearer tokens in tool call headers"
)
logger.info(
"Expecting Authorization bearer tokens in tool call headers"
"Protected resource metadata points to Google's authorization server"
)
else:
# Standard OAuth 2.1 mode: use FastMCP's GoogleProvider
Expand Down