diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index 3e684ac..8c332c3 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -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( diff --git a/nextcloud_mcp_server/auth/session_backend.py b/nextcloud_mcp_server/auth/session_backend.py index 42a03d5..1a3dc71 100644 --- a/nextcloud_mcp_server/auth/session_backend.py +++ b/nextcloud_mcp_server/auth/session_backend.py @@ -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")