From 9be8901542853b48bdb3ef23fa2e111591592e66 Mon Sep 17 00:00:00 2001 From: sriramveeraghanta Date: Fri, 19 Jun 2026 18:21:37 +0530 Subject: [PATCH] fix: verify header API key against Plane API PlaneHeaderAuthProvider previously trusted any x-api-key header without validation, only logging that a key was present. This verifies the key by calling /api/v1/users/me/ on the Plane API and rejecting the token if the request does not return 200. - Resolve base URL from PLANE_INTERNAL_BASE_URL / PLANE_BASE_URL (default https://api.plane.so) for server-to-server verification. - Add configurable request timeout (default 10s). - Handle httpx.RequestError by failing closed (return None). --- plane_mcp/auth/plane_header_auth_provider.py | 29 ++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/plane_mcp/auth/plane_header_auth_provider.py b/plane_mcp/auth/plane_header_auth_provider.py index 9cedd0f..9ead222 100644 --- a/plane_mcp/auth/plane_header_auth_provider.py +++ b/plane_mcp/auth/plane_header_auth_provider.py @@ -1,15 +1,23 @@ +import os import time +import httpx from fastmcp.server.auth import TokenVerifier from fastmcp.server.auth.auth import AccessToken from fastmcp.utilities.logging import get_logger logger = get_logger(__name__) +DEFAULT_PLANE_BASE_URL = "https://api.plane.so" + class PlaneHeaderAuthProvider(TokenVerifier): - def __init__(self, required_scopes: list[str] | None = None): + def __init__(self, required_scopes: list[str] | None = None, timeout_seconds: int = 10): super().__init__(required_scopes=required_scopes) + self.timeout_seconds = timeout_seconds + self.plane_base_url = ( + os.getenv("PLANE_INTERNAL_BASE_URL") or os.getenv("PLANE_BASE_URL", DEFAULT_PLANE_BASE_URL) + ).rstrip("/") async def verify_token(self, token: str) -> AccessToken | None: try: @@ -20,7 +28,21 @@ async def verify_token(self, token: str) -> AccessToken | None: if token: workspace_slug = headers.get("x-workspace-slug") if workspace_slug: - logger.info("Using API key from HTTP headers") + logger.info("Verifying API key against Plane API") + user_url = f"{self.plane_base_url}/api/v1/users/me/" + async with httpx.AsyncClient(timeout=self.timeout_seconds) as client: + response = await client.get( + user_url, + headers={ + "x-api-key": token, + "Content-Type": "application/json", + }, + ) + if response.status_code != 200: + logger.warning("API key verification failed: %s", response.status_code) + return None + + logger.info("API key verified successfully") expires_at = int(time.time() + 3600) return AccessToken( token=token, @@ -34,6 +56,9 @@ async def verify_token(self, token: str) -> AccessToken | None: ) else: logger.warning("x-api-key header found but x-workspace-slug is missing") + except httpx.RequestError as e: + logger.warning("API key verification request failed: %s", e) + return None except RuntimeError: # No active HTTP request available (e.g., stdio transport) logger.debug("No active HTTP request available for header check")