From 06dde19de4f457ae1f451d5f008359bb6ce35617 Mon Sep 17 00:00:00 2001 From: Anthony Date: Fri, 31 Oct 2025 23:08:44 +0000 Subject: [PATCH 1/2] fix: support capability tokens in middleware for MCP server file access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables MCP servers to authenticate using capability tokens when accessing files through /api/files/download/ endpoints. Previously, the middleware only checked for X-User-Email headers, causing MCP server requests to fail in production nginx environments. Changes: - Check capability tokens before X-User-Email header validation - Return 401 for unauthenticated API requests instead of redirecting - Maintain backward compatibility with browser-based authentication Fixes authentication flow for MCP servers making programmatic file requests with tokenized URLs while preserving existing browser-based auth behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/core/middleware.py | 41 +++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 96ca289..7e2deea 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -2,12 +2,13 @@ import logging -from fastapi import Request +from fastapi import HTTPException, Request from fastapi.responses import RedirectResponse from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import Response from core.auth import get_user_from_header +from core.capabilities import verify_file_token from infrastructure.app_factory import app_factory logger = logging.getLogger(__name__) @@ -23,12 +24,29 @@ def __init__(self, app, debug_mode: bool = False): async def dispatch(self, request: Request, call_next) -> Response: # Log request logger.info(f"Request: {request.method} {request.url.path}") - + # Skip auth for static files and auth endpoint if request.url.path.startswith('/static') or request.url.path == '/auth': return await call_next(request) - - # Check authentication + + # Check for capability token in download URLs (allows MCP servers to access files) + if request.url.path.startswith('/api/files/download/'): + token = request.query_params.get('token') + if token: + claims = verify_file_token(token) + if claims: + # Valid capability token - extract user from token and allow request + user_email = claims.get('u') + if user_email: + logger.info(f"Authenticated via capability token for user: {user_email}") + request.state.user_email = user_email + return await call_next(request) + else: + logger.warning("Valid token but missing user email claim") + else: + logger.warning("Invalid capability token provided") + + # Check authentication via X-User-Email header user_email = None if self.debug_mode: # In debug mode, honor X-User-Email header if provided, otherwise use config test user @@ -43,13 +61,18 @@ async def dispatch(self, request: Request, call_next) -> Response: else: x_email_header = request.headers.get('X-User-Email') user_email = get_user_from_header(x_email_header) - + if not user_email: - logger.warning("Missing X-User-Email, redirecting to auth") - return RedirectResponse(url="/auth", status_code=302) - + # Distinguish between API endpoints (return 401) and browser endpoints (redirect) + if request.url.path.startswith('/api/'): + logger.warning(f"Missing authentication for API endpoint: {request.url.path}") + raise HTTPException(status_code=401, detail="Unauthorized") + else: + logger.warning("Missing X-User-Email, redirecting to auth") + return RedirectResponse(url="/auth", status_code=302) + # Add user to request state request.state.user_email = user_email - + response = await call_next(request) return response \ No newline at end of file From 398c0fd9fe7ccf3e398362810aa40f24e61d41db Mon Sep 17 00:00:00 2001 From: Anthony Date: Fri, 31 Oct 2025 23:17:25 +0000 Subject: [PATCH 2/2] docs: add comment explaining separation of auth/authz in middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarifies why the middleware only validates capability token authenticity but not file key matching. The route handler validates file key authorization, maintaining proper separation of concerns between authentication (middleware) and authorization (route handlers). Addresses review feedback from Copilot AI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/core/middleware.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 7e2deea..48759d8 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -36,6 +36,10 @@ async def dispatch(self, request: Request, call_next) -> Response: claims = verify_file_token(token) if claims: # Valid capability token - extract user from token and allow request + # Note: We only validate token authenticity here (authentication). + # The route handler validates that token's file key matches the requested + # file (authorization). This separation of concerns keeps middleware focused + # on authentication while route handlers handle resource-specific authorization. user_email = claims.get('u') if user_email: logger.info(f"Authenticated via capability token for user: {user_email}")