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 16:33:33 +01:00
committed by Chris Coutinho
parent 3b4606b798
commit d768909fd4
7 changed files with 804 additions and 60 deletions
+3
View File
@@ -6,6 +6,7 @@ from mcp.shared.exceptions import McpError
from mcp.types import ErrorData
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.auth.provisioning_decorator import require_provisioning
from nextcloud_mcp_server.context import get_client
from nextcloud_mcp_server.models.notes import (
AppendContentResponse,
@@ -86,6 +87,7 @@ def configure_notes_tools(mcp: FastMCP):
@mcp.tool()
@require_scopes("notes:write")
@require_provisioning
async def nc_notes_create_note(
title: str, content: str, category: str, ctx: Context
) -> CreateNoteResponse:
@@ -247,6 +249,7 @@ def configure_notes_tools(mcp: FastMCP):
@mcp.tool()
@require_scopes("notes:read")
@require_provisioning
async def nc_notes_search_notes(query: str, ctx: Context) -> SearchNotesResponse:
"""Search notes by title or content, returning only id, title, and category (requires notes:read scope)."""
client = get_client(ctx)