Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions backend/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -23,12 +24,33 @@ 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
# 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}")
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
Expand All @@ -43,13 +65,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
Loading