10dffd0c10
SessionAuthBackend middleware was wrapping the entire app including FastMCP, which prevented FastMCP's OAuth token verification from running properly. When SessionAuthBackend returned None for /mcp paths, Starlette marked requests as "anonymous" and allowed them through, bypassing FastMCP's authentication. Changes: 1. Route restructuring (app.py): - Create separate Starlette app for browser routes (/user, /user/page) - Apply SessionAuthBackend only to browser app - Mount browser app at /user/* before FastMCP - Mount FastMCP at / (catch-all with its own OAuth) - Remove global SessionAuthBackend middleware 2. SessionAuthBackend cleanup (session_backend.py): - Remove path exclusion logic (no longer needed) - Simplify to only handle browser routes - Update docstring to reflect mount-based isolation Benefits: - FastMCP's OAuth token verification now runs properly - No middleware interference between authentication mechanisms - Clear separation: SessionAuth for browser UI, OAuth Bearer for MCP clients - Tests confirm OAuth authentication works correctly Testing: - All OAuth tests pass (test_mcp_oauth_*, test_jwt_*) - Browser routes still require session auth - FastMCP routes use OAuth Bearer tokens exclusively 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
"""Session-based authentication backend for Starlette routes.
|
|
|
|
Provides browser-based authentication for admin UI routes, separate from
|
|
MCP's OAuth authentication flow.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
|
|
from starlette.authentication import (
|
|
AuthCredentials,
|
|
AuthenticationBackend,
|
|
SimpleUser,
|
|
)
|
|
from starlette.requests import HTTPConnection
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SessionAuthBackend(AuthenticationBackend):
|
|
"""Authentication backend using signed session cookies.
|
|
|
|
For BasicAuth mode: Always authenticates as the configured user.
|
|
For OAuth mode: Checks for valid session cookie with stored refresh token.
|
|
"""
|
|
|
|
def __init__(self, oauth_enabled: bool = False):
|
|
"""Initialize session authentication backend.
|
|
|
|
Args:
|
|
oauth_enabled: Whether OAuth mode is enabled
|
|
"""
|
|
self.oauth_enabled = oauth_enabled
|
|
|
|
async def authenticate(
|
|
self, conn: HTTPConnection
|
|
) -> tuple[AuthCredentials, SimpleUser] | None:
|
|
"""Authenticate the request based on session cookie or BasicAuth mode.
|
|
|
|
This backend is only applied to browser routes (/user/*) via a separate
|
|
Starlette app mount. FastMCP routes use their own OAuth Bearer token
|
|
authentication.
|
|
|
|
Args:
|
|
conn: HTTP connection
|
|
|
|
Returns:
|
|
Tuple of (credentials, user) if authenticated, None otherwise
|
|
"""
|
|
# BasicAuth mode: Always authenticated as the configured user
|
|
if not self.oauth_enabled:
|
|
username = os.getenv("NEXTCLOUD_USERNAME", "admin")
|
|
return AuthCredentials(["authenticated", "admin"]), SimpleUser(username)
|
|
|
|
# OAuth mode: Check for session cookie
|
|
session_id = conn.cookies.get("mcp_session")
|
|
logger.info(
|
|
f"Session authentication check - cookie present: {session_id is not None}, path: {conn.url.path}"
|
|
)
|
|
if not session_id:
|
|
logger.info("No session cookie found - redirecting to login")
|
|
return None
|
|
|
|
logger.info(f"Found session cookie: {session_id[:16]}...")
|
|
|
|
# Get OAuth context from app state
|
|
oauth_context = getattr(conn.app.state, "oauth_context", None)
|
|
if not oauth_context:
|
|
logger.warning("OAuth context not available in app state")
|
|
return None
|
|
|
|
# Validate session
|
|
storage = oauth_context.get("storage")
|
|
if not storage:
|
|
logger.warning("OAuth storage not available")
|
|
return None
|
|
|
|
try:
|
|
# Check if user has refresh token (indicates logged-in session)
|
|
logger.info(f"Looking up refresh token for session: {session_id[:16]}...")
|
|
token_data = await storage.get_refresh_token(session_id)
|
|
if not token_data:
|
|
logger.warning(
|
|
f"No refresh token found for session {session_id[:16]}..."
|
|
)
|
|
return None
|
|
|
|
# Session is valid - use session_id (which is user_id from ID token) as username
|
|
username = session_id
|
|
logger.info(f"✓ Session authenticated successfully: {username[:16]}...")
|
|
|
|
return AuthCredentials(["authenticated"]), SimpleUser(username)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Session validation error: {e}")
|
|
return None
|