feat(auth): add multi-user BasicAuth pass-through mode
Implement multi-user BasicAuth pass-through mode (ADR-020) where each request includes BasicAuth credentials that are forwarded to Nextcloud APIs without persistent storage. Changes: - Add _get_client_from_basic_auth() in context.py to extract credentials from Authorization header (set by BasicAuthMiddleware) - Add AstrolabeClient for app password provisioning via Astrolabe API - Update oauth_sync.py with dual credential support (app passwords first, then refresh tokens as fallback) - Simplify oauth_tools.py provisioning logic - Add integration tests for app password provisioning and multi-user BasicAuth Features: - Stateless multi-user mode: credentials passed per-request - Optional background sync via app passwords (stored in Astrolabe) - Falls back to refresh tokens if app password not available - Test coverage for provisioning flow and pass-through mode Related: ADR-019 (Multi-user BasicAuth), ADR-020 (Deployment Modes) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,11 @@ async def get_client(ctx: Context) -> NextcloudClient:
|
||||
return _get_client_from_session_config(ctx)
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Multi-user BasicAuth pass-through mode - extract credentials from request
|
||||
if settings.enable_multi_user_basic_auth:
|
||||
return _get_client_from_basic_auth(ctx)
|
||||
|
||||
lifespan_ctx = ctx.request_context.lifespan_context
|
||||
|
||||
# BasicAuth mode - use shared client (no token exchange)
|
||||
@@ -177,3 +182,67 @@ def _get_client_from_session_config(ctx: Context) -> NextcloudClient:
|
||||
username=username,
|
||||
auth=BasicAuth(username, app_password),
|
||||
)
|
||||
|
||||
|
||||
def _get_client_from_basic_auth(ctx: Context) -> NextcloudClient:
|
||||
"""
|
||||
Create NextcloudClient from BasicAuth credentials in request headers.
|
||||
|
||||
For multi-user BasicAuth pass-through mode, this function extracts
|
||||
username/password from the Authorization: Basic header (stored by
|
||||
BasicAuthMiddleware) and creates a client that passes these credentials
|
||||
through to Nextcloud APIs.
|
||||
|
||||
The credentials are NOT stored persistently - they exist only for the
|
||||
duration of this request (stateless).
|
||||
|
||||
Args:
|
||||
ctx: MCP request context with basic_auth in request state
|
||||
|
||||
Returns:
|
||||
NextcloudClient configured with BasicAuth credentials
|
||||
|
||||
Raises:
|
||||
ValueError: If BasicAuth credentials not found in request or if
|
||||
NEXTCLOUD_HOST is not configured
|
||||
"""
|
||||
settings = get_settings()
|
||||
|
||||
# Validate that NEXTCLOUD_HOST is configured
|
||||
if not settings.nextcloud_host:
|
||||
raise ValueError(
|
||||
"NEXTCLOUD_HOST environment variable must be set for multi-user BasicAuth mode"
|
||||
)
|
||||
|
||||
# Extract BasicAuth credentials from request state (set by BasicAuthMiddleware)
|
||||
# Access scope through the request object
|
||||
scope = getattr(ctx.request_context.request, "scope", None)
|
||||
if scope is None:
|
||||
raise ValueError("Request scope not available in context")
|
||||
|
||||
request_state = scope.get("state", {})
|
||||
basic_auth = request_state.get("basic_auth")
|
||||
|
||||
if not basic_auth:
|
||||
raise ValueError(
|
||||
"BasicAuth credentials not found in request. "
|
||||
"Ensure Authorization: Basic header is provided with valid credentials."
|
||||
)
|
||||
|
||||
username = basic_auth.get("username")
|
||||
password = basic_auth.get("password")
|
||||
|
||||
if not username or not password:
|
||||
raise ValueError("Invalid BasicAuth credentials - missing username or password")
|
||||
|
||||
logger.debug(
|
||||
f"Creating multi-user BasicAuth client for {settings.nextcloud_host} as {username}"
|
||||
)
|
||||
|
||||
# Create client that passes BasicAuth credentials through to Nextcloud
|
||||
# settings.nextcloud_host is guaranteed to be str after the check above
|
||||
return NextcloudClient(
|
||||
base_url=settings.nextcloud_host,
|
||||
username=username,
|
||||
auth=BasicAuth(username, password),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user