fix: allow OAuth Bearer tokens on /mcp endpoint by excluding from session auth

SessionAuthBackend was blocking MCP clients using OAuth Bearer tokens because
it returned None when no session cookie was present, causing 401 responses
before FastMCP's OAuth provider could validate Bearer tokens.

Changes:
- Add path-based exclusion to SessionAuthBackend.authenticate()
- Skip session auth for paths using other authentication methods:
  - /mcp (FastMCP OAuth Bearer tokens)
  - /.well-known/oauth-protected-resource (public PRM endpoint)
  - /health/live, /health/ready (public health checks)
  - /oauth/login, /oauth/login-callback, /oauth/authorize (OAuth flow pages)
- Browser routes (/user, /user/page, /oauth/logout) still require session cookies

This allows MCP clients to connect with OAuth Bearer tokens while maintaining
session-based authentication for browser UI routes.

Testing:
- OAuth tests pass (test_mcp_oauth_server_connection, etc.)
- Browser routes still require session auth (/user returns 303 redirect)
- Public endpoints remain accessible (/health/live works)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-11-04 03:26:13 +01:00
parent 192c4bf009
commit 737d62fe91
@@ -37,12 +37,33 @@ 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.
Args:
conn: HTTP connection
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")