feat: Implement ADR-004 Progressive Consent foundation (partial)

Implements Progressive Consent architecture with dual OAuth flows:
- Flow 1: Direct client authentication (aud: "mcp-server")
- Flow 2: Resource provisioning with refresh tokens

Components added:
- Client registry with validation (client_registry.py)
- Progressive token verifier (progressive_token_verifier.py)
- Token broker service integration
- Provisioning decorator for MCP tools
- OAuth provisioning tools (provision_nextcloud_access, etc.)

Configuration:
- Progressive Consent enabled by default (ENABLE_PROGRESSIVE_CONSENT=true)
- Client validation with pre-registered clients
- Audience separation framework

KNOWN ISSUE - Token Exchange Pattern Incorrect:
The current implementation does NOT properly implement token exchange.
MCP session tokens should be EXCHANGED for delegated Nextcloud tokens
during tool calls, not stored/reused. Critical corrections needed:

1. Session tokens: Flow 1 token → exchange → ephemeral Nextcloud token
   - Generated on-demand per tool call
   - Short-lived, not stored
   - Scopes limited to tool requirements

2. Background tokens: Flow 2 refresh token → background Nextcloud token
   - Only for offline/background jobs
   - Potentially different scopes than session tokens
   - Must NOT be used for MCP session tool calls

The token exchange mechanism needs to be implemented to properly
separate session-time delegation from background job authorization.

🤖 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-03 20:33:55 +01:00
committed by Chris Coutinho
co-authored by Claude
parent 3b4606b798
commit d768909fd4
7 changed files with 804 additions and 60 deletions
+61 -2
View File
@@ -27,6 +27,9 @@ from nextcloud_mcp_server.auth import (
has_required_scopes,
is_jwt_token,
)
from nextcloud_mcp_server.auth.progressive_token_verifier import (
ProgressiveConsentTokenVerifier,
)
from nextcloud_mcp_server.client import NextcloudClient
from nextcloud_mcp_server.config import (
LOGGING_CONFIG,
@@ -45,6 +48,7 @@ from nextcloud_mcp_server.server import (
configure_tables_tools,
configure_webdav_tools,
)
from nextcloud_mcp_server.server.oauth_tools import register_oauth_tools
logger = logging.getLogger(__name__)
@@ -211,7 +215,9 @@ class OAuthAppContext:
"""Application context for OAuth mode."""
nextcloud_host: str
token_verifier: NextcloudTokenVerifier
token_verifier: (
object # Can be NextcloudTokenVerifier or ProgressiveConsentTokenVerifier
)
refresh_token_storage: Optional["RefreshTokenStorage"] = None
oauth_client: Optional[object] = None # NextcloudOAuthClient or KeycloakOAuthClient
oauth_provider: str = "nextcloud" # "nextcloud" or "keycloak"
@@ -558,8 +564,52 @@ async def setup_oauth_config():
jwt_validation_issuer = issuer
client_issuer = issuer
# Check if Progressive Consent mode is enabled
enable_progressive = (
os.getenv("ENABLE_PROGRESSIVE_CONSENT", "true").lower() == "true"
)
# Create token verifier
if is_external_idp:
if enable_progressive:
# Progressive Consent mode: Use specialized verifier with audience separation
logger.info("✓ Progressive Consent mode enabled - dual OAuth flows active")
# Get encryption key for token broker
encryption_key = os.getenv("TOKEN_ENCRYPTION_KEY")
if not encryption_key:
logger.warning(
"TOKEN_ENCRYPTION_KEY not set - token broker will not be available"
)
# Create token broker service
from nextcloud_mcp_server.auth.token_broker import TokenBrokerService
token_broker = None
if encryption_key and refresh_token_storage:
token_broker = TokenBrokerService(
storage=refresh_token_storage,
oidc_discovery_url=discovery_url,
nextcloud_host=nextcloud_host,
encryption_key=encryption_key,
)
logger.info(
"✓ Token Broker service initialized for audience-specific tokens"
)
# Create Progressive Consent token verifier
token_verifier = ProgressiveConsentTokenVerifier(
token_storage=refresh_token_storage,
token_broker=token_broker,
oidc_discovery_url=discovery_url,
nextcloud_host=nextcloud_host,
encryption_key=encryption_key,
)
logger.info(
"✓ Progressive Consent verifier configured - enforcing audience separation"
)
elif is_external_idp:
# External IdP mode: Validate via Nextcloud user_oidc app
# The user_oidc app accepts tokens from the external IdP and provisions users
nextcloud_userinfo_uri = f"{nextcloud_host}/apps/user_oidc/userinfo"
@@ -761,6 +811,15 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
f"Unknown app: {app_name}. Available apps: {list(available_apps.keys())}"
)
# Register OAuth provisioning tools if in OAuth mode with Progressive Consent
if oauth_enabled:
enable_progressive = (
os.getenv("ENABLE_PROGRESSIVE_CONSENT", "true").lower() == "true"
)
if enable_progressive:
logger.info("Registering OAuth provisioning tools for Progressive Consent")
register_oauth_tools(mcp)
# Override list_tools to filter based on user's token scopes (OAuth mode only)
if oauth_enabled:
original_list_tools = mcp._tool_manager.list_tools