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) <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-03-30 11:42:31 +00:00
parent 50c3b116f7
commit 5d08e15b0f

View file

@ -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). "