9fab6cb550
Replace two non-compliant token verifiers (NextcloudTokenVerifier and
ProgressiveConsentTokenVerifier) with a single UnifiedTokenVerifier that properly
validates token audiences per MCP Security Best Practices specification.
The previous implementation had a critical security vulnerability where tokens
intended for the MCP server were passed directly to Nextcloud APIs without
proper audience validation (token passthrough anti-pattern). This violates
OAuth 2.0 security principles and the MCP specification.
Changes:
- Add UnifiedTokenVerifier supporting two compliant modes:
* Multi-audience mode (default): Validates tokens contain BOTH MCP and
Nextcloud audiences, enabling direct use without exchange
* Token exchange mode (opt-in): Validates MCP audience only, exchanges
for Nextcloud tokens via RFC 8693 with caching to minimize latency
- Remove token passthrough vulnerability from context.py and context_helper.py
- Implement token exchange caching (5-minute TTL default) to reduce network calls
- Add required environment variables for audience validation:
* NEXTCLOUD_MCP_SERVER_URL - MCP server URL (used as audience)
* NEXTCLOUD_RESOURCE_URI - Nextcloud resource identifier
* TOKEN_EXCHANGE_CACHE_TTL - Cache TTL for exchanged tokens
- Update docker-compose.yml with resource URI configuration for both OAuth modes
- Add comprehensive test suite (29 tests) covering both authentication modes
- Remove legacy NextcloudTokenVerifier and ProgressiveConsentTokenVerifier
Security improvements:
- Eliminates token passthrough anti-pattern
- Enforces proper audience separation between MCP and Nextcloud
- Complies with MCP Security Best Practices and RFC 8707/8693
- Maintains performance with token exchange caching
Test results: 65/65 unit tests passed, 5/5 smoke tests passed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
"""Helper functions for accessing context in MCP tools."""
|
|
|
|
from mcp.server.fastmcp import Context
|
|
|
|
from nextcloud_mcp_server.client import NextcloudClient
|
|
from nextcloud_mcp_server.config import get_settings
|
|
|
|
|
|
async def get_client(ctx: Context) -> NextcloudClient:
|
|
"""
|
|
Get the appropriate Nextcloud client based on authentication mode.
|
|
|
|
ADR-005 compliant implementation supporting two modes:
|
|
1. BasicAuth mode: Returns shared client from lifespan context
|
|
2. Multi-audience mode (ENABLE_TOKEN_EXCHANGE=false, default):
|
|
Token already contains both MCP and Nextcloud audiences - use directly
|
|
3. Token exchange mode (ENABLE_TOKEN_EXCHANGE=true):
|
|
Exchange MCP token for Nextcloud token via RFC 8693
|
|
|
|
SECURITY: Token passthrough has been REMOVED. All OAuth modes validate
|
|
proper token audiences per MCP Security Best Practices specification.
|
|
|
|
Note: Nextcloud doesn't support OAuth scopes natively. Scopes are enforced
|
|
by the MCP server via @require_scopes decorator, not by the IdP.
|
|
|
|
This function automatically detects the authentication mode by checking
|
|
the type of the lifespan context.
|
|
|
|
Args:
|
|
ctx: MCP request context
|
|
|
|
Returns:
|
|
NextcloudClient configured for the current authentication mode
|
|
|
|
Raises:
|
|
AttributeError: If context doesn't contain expected data
|
|
|
|
Example:
|
|
```python
|
|
@mcp.tool()
|
|
async def my_tool(ctx: Context):
|
|
client = await get_client(ctx)
|
|
return await client.capabilities()
|
|
```
|
|
"""
|
|
settings = get_settings()
|
|
lifespan_ctx = ctx.request_context.lifespan_context
|
|
|
|
# BasicAuth mode - use shared client (no token exchange)
|
|
if hasattr(lifespan_ctx, "client"):
|
|
return lifespan_ctx.client
|
|
|
|
# OAuth mode (has 'nextcloud_host' attribute)
|
|
if hasattr(lifespan_ctx, "nextcloud_host"):
|
|
from nextcloud_mcp_server.auth.context_helper import (
|
|
get_client_from_context,
|
|
get_session_client_from_context,
|
|
)
|
|
|
|
if settings.enable_token_exchange:
|
|
# Mode 2: Exchange MCP token for Nextcloud token
|
|
# Token was validated to have MCP audience in UnifiedTokenVerifier
|
|
# Now exchange it for Nextcloud audience
|
|
return await get_session_client_from_context(
|
|
ctx, lifespan_ctx.nextcloud_host
|
|
)
|
|
else:
|
|
# Mode 1: Multi-audience token - use directly
|
|
# Token was validated to have BOTH audiences in UnifiedTokenVerifier
|
|
return get_client_from_context(ctx, lifespan_ctx.nextcloud_host)
|
|
|
|
# Unknown context type
|
|
raise AttributeError(
|
|
f"Lifespan context does not have 'client' or 'nextcloud_host' attribute. "
|
|
f"Type: {type(lifespan_ctx)}"
|
|
)
|