fix: restructure routes to prevent SessionAuthBackend from interfering with FastMCP OAuth
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>
This commit is contained in:
+22
-12
@@ -1008,27 +1008,37 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
||||
)
|
||||
|
||||
# Add user info routes (available in both BasicAuth and OAuth modes)
|
||||
# These require session authentication, so we wrap them in a separate app
|
||||
from nextcloud_mcp_server.auth.session_backend import SessionAuthBackend
|
||||
from nextcloud_mcp_server.auth.userinfo_routes import (
|
||||
user_info_html,
|
||||
user_info_json,
|
||||
)
|
||||
|
||||
routes.append(Route("/user", user_info_json, methods=["GET"]))
|
||||
routes.append(Route("/user/page", user_info_html, methods=["GET"]))
|
||||
logger.info("User info routes enabled: /user (JSON), /user/page (HTML)")
|
||||
# Create a separate Starlette app for browser routes that need session auth
|
||||
# This prevents SessionAuthBackend from interfering with FastMCP's OAuth
|
||||
browser_routes = [
|
||||
Route("/", user_info_json, methods=["GET"]), # /user/ → user_info_json
|
||||
Route("/page", user_info_html, methods=["GET"]), # /user/page → user_info_html
|
||||
]
|
||||
|
||||
routes.append(Mount("/", app=mcp_app))
|
||||
app = Starlette(routes=routes, lifespan=starlette_lifespan)
|
||||
|
||||
# Add authentication middleware for browser-based routes
|
||||
from nextcloud_mcp_server.auth.session_backend import SessionAuthBackend
|
||||
|
||||
# SessionAuthBackend will look up oauth_context from app.state at runtime
|
||||
app.add_middleware(
|
||||
browser_app = Starlette(routes=browser_routes)
|
||||
browser_app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
backend=SessionAuthBackend(oauth_enabled=oauth_enabled),
|
||||
)
|
||||
logger.info("Authentication middleware enabled for browser routes")
|
||||
|
||||
# Mount browser app at /user (so /user and /user/page work)
|
||||
routes.append(Mount("/user", app=browser_app))
|
||||
logger.info("User info routes with session auth: /user, /user/page")
|
||||
|
||||
# Mount FastMCP at root last (catch-all, handles OAuth via token_verifier)
|
||||
routes.append(Mount("/", app=mcp_app))
|
||||
|
||||
app = Starlette(routes=routes, lifespan=starlette_lifespan)
|
||||
logger.info(
|
||||
"Routes: /user/* with SessionAuth, /mcp with FastMCP OAuth Bearer tokens"
|
||||
)
|
||||
|
||||
# Add CORS middleware to allow browser-based clients like MCP Inspector
|
||||
app.add_middleware(
|
||||
|
||||
@@ -37,9 +37,9 @@ class SessionAuthBackend(AuthenticationBackend):
|
||||
) -> tuple[AuthCredentials, SimpleUser] | None:
|
||||
"""Authenticate the request based on session cookie or BasicAuth mode.
|
||||
|
||||
For paths that use other authentication mechanisms (OAuth Bearer tokens,
|
||||
public endpoints), this backend returns None to skip session authentication
|
||||
and allow those mechanisms to handle the request.
|
||||
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
|
||||
@@ -47,23 +47,6 @@ class SessionAuthBackend(AuthenticationBackend):
|
||||
Returns:
|
||||
Tuple of (credentials, user) if authenticated, None otherwise
|
||||
"""
|
||||
# Skip session auth for paths that use other authentication methods
|
||||
# or are publicly accessible
|
||||
excluded_paths = [
|
||||
"/mcp", # FastMCP OAuth Bearer tokens (handled by FastMCP's auth provider)
|
||||
"/.well-known/oauth-protected-resource", # Public PRM metadata
|
||||
"/health/live", # Health checks (public)
|
||||
"/health/ready",
|
||||
"/oauth/login", # Login flow (no auth required to access login page)
|
||||
"/oauth/login-callback", # OAuth callback (receives code from IdP)
|
||||
"/oauth/authorize", # Flow 1 authorize endpoint (no session required)
|
||||
]
|
||||
|
||||
if any(conn.url.path.startswith(path) for path in excluded_paths):
|
||||
# Don't interfere - let other auth mechanisms handle these paths
|
||||
logger.debug(f"Skipping session auth for excluded path: {conn.url.path}")
|
||||
return None
|
||||
|
||||
# BasicAuth mode: Always authenticated as the configured user
|
||||
if not self.oauth_enabled:
|
||||
username = os.getenv("NEXTCLOUD_USERNAME", "admin")
|
||||
|
||||
Reference in New Issue
Block a user