From 5d08e15b0f969c10170c37e493a88ea517940f13 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Mon, 30 Mar 2026 11:42:31 +0000 Subject: [PATCH] security: replace open OAuth with Forgejo-backed authentication Uses FastMCP OAuthProxy to proxy OAuth to Forgejo (git.georgsen.dk). Only users who can authenticate with Forgejo get MCP access. DCR is still used for client registration, but authorization requires Forgejo login. Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp_bridge/mcp_server.py | 51 +++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/mcp_bridge/mcp_server.py b/mcp_bridge/mcp_server.py index 5925f35..391579f 100644 --- a/mcp_bridge/mcp_server.py +++ b/mcp_bridge/mcp_server.py @@ -5,27 +5,66 @@ import logging from datetime import datetime, timezone from fastmcp import FastMCP -from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider -from fastmcp.server.auth.auth import ClientRegistrationOptions +import contextlib +import httpx +from fastmcp.server.auth import TokenVerifier, AccessToken +from fastmcp.server.auth.oauth_proxy import OAuthProxy from starlette.requests import Request from starlette.responses import JSONResponse from starlette.routing import Route from .db import Database -from .config import get_group_chat_id +from .config import get_group_chat_id, load_credentials logger = logging.getLogger(__name__) db: Database | None = None -oauth = InMemoryOAuthProvider( +FORGEJO_URL = "https://git.georgsen.dk" + + +class ForgejoTokenVerifier(TokenVerifier): + """Verify OAuth tokens against Forgejo's API.""" + + def __init__(self, forgejo_url: str = FORGEJO_URL): + super().__init__(required_scopes=None) + self.forgejo_url = forgejo_url + + async def verify_token(self, token: str) -> AccessToken | None: + try: + async with httpx.AsyncClient(timeout=10) as client: + resp = await client.get( + f"{self.forgejo_url}/api/v1/user", + headers={"Authorization": f"Bearer {token}"}, + ) + if resp.status_code != 200: + return None + user = resp.json() + return AccessToken( + token=token, + client_id=str(user["id"]), + scopes=[], + expires_at=None, + claims={"sub": str(user["id"]), "login": user.get("login")}, + ) + except Exception as e: + logger.debug(f"Forgejo token verification failed: {e}") + return None + + +creds = load_credentials() +auth = OAuthProxy( + upstream_authorization_endpoint=f"{FORGEJO_URL}/login/oauth/authorize", + upstream_token_endpoint=f"{FORGEJO_URL}/login/oauth/access_token", + upstream_client_id=creds["FORGEJO_OAUTH_CLIENT_ID"], + upstream_client_secret=creds["FORGEJO_OAUTH_CLIENT_SECRET"], + token_verifier=ForgejoTokenVerifier(), base_url="https://mcp.georgsen.dk", - client_registration_options=ClientRegistrationOptions(enabled=True), ) mcp = FastMCP( name="homelab-bridge", - auth=oauth, + auth=auth, instructions=( "This MCP server bridges claude.ai to a homelab Telegram group chat. " "Use pull_updates to read conversation history (supports cursor-based pagination). "