You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

82 lines
2.9 KiB
Python

from __future__ import annotations
from dataclasses import replace
import anyio
from fastapi import HTTPException
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings
from app.config import Settings
from app.security import AuthBackend
_MCP_ALWAYS_ENABLED_SCOPES = ["availability:read", "available_meeting_intervals:read"]
def resolve_mcp_auth_mode(settings: Settings) -> str:
if settings.mcp_auth_mode == "inherit":
return settings.auth_mode
return settings.mcp_auth_mode
def mcp_supported_scopes(settings: Settings) -> list[str]:
scopes = list(_MCP_ALWAYS_ENABLED_SCOPES)
if settings.mcp_enable_mutation_tools:
scopes.extend(["mail:scan", "unsubscribe:read", "unsubscribe:execute"])
return scopes
def build_mcp_oauth_auth_settings(settings: Settings) -> AuthSettings:
if not settings.mcp_oauth_issuer:
raise ValueError("MCP_OAUTH_ISSUER is required when MCP_AUTH_MODE=oauth.")
if not settings.mcp_resource_server_url:
raise ValueError("MCP_RESOURCE_SERVER_URL is required when MCP_AUTH_MODE=oauth.")
return AuthSettings(
issuer_url=settings.mcp_oauth_issuer,
resource_server_url=settings.mcp_resource_server_url,
# Leave transport-level scopes unset and enforce scope checks per tool.
required_scopes=None,
)
def build_mcp_oauth_token_verifier(settings: Settings) -> TokenVerifier:
oauth_settings = replace(settings, auth_mode="oauth")
auth_backend = AuthBackend(
settings=oauth_settings,
oauth_introspection_url=settings.mcp_oauth_introspection_url,
oauth_client_id=settings.mcp_oauth_client_id,
oauth_client_secret=settings.mcp_oauth_client_secret or None,
oauth_issuer=settings.mcp_oauth_issuer,
oauth_audience=settings.mcp_oauth_audience,
oauth_timeout_seconds=settings.mcp_oauth_timeout_seconds,
)
return OAuthIntrospectionTokenVerifier(auth_backend)
class OAuthIntrospectionTokenVerifier(TokenVerifier):
"""FastMCP TokenVerifier backed by MCP OAuth introspection settings."""
def __init__(self, auth_backend: AuthBackend) -> None:
self._auth_backend = auth_backend
async def verify_token(self, token: str) -> AccessToken | None:
auth_context = await anyio.to_thread.run_sync(self._authenticate_token, token)
if auth_context is None:
return None
scopes = sorted(scope for scope in auth_context.scopes if scope and scope != "*")
return AccessToken(
token=token,
client_id=auth_context.subject,
scopes=scopes,
)
def _authenticate_token(self, token: str):
try:
return self._auth_backend.authenticate(
x_api_key=None,
authorization=f"Bearer {token}",
required_scopes=set(),
)
except HTTPException:
return None