docs: Update ADR-004 with progressive consent architecture

Refactor ADR-004 to document the proper OAuth architecture where MCP
clients are registered at the IdP level (not with MCP server) and use
a progressive consent pattern with dual OAuth flows.

## Key Changes

### MCP Client Registration
- Document that MCP clients (Claude Desktop, etc.) register at IdP level
- Show DCR and pre-registration options
- Clarify client validation happens against IdP registry

### Progressive Consent Architecture
Replace single "Hybrid Flow" with three-phase progressive consent:

**Phase 1: MCP Client Authentication** (Always)
- MCP client uses own client_id (e.g., "claude-desktop")
- User consents to "Claude Desktop accessing MCP Server"
- MCP server validates client exists at IdP
- Stores MCP client access token

**Phase 2: Nextcloud Consent** (Conditional)
- Only if MCP server doesn't have refresh token for user
- MCP server uses own client_id ("nextcloud-mcp-server")
- User consents to "MCP Server accessing Nextcloud offline"
- MCP server stores master refresh token
- SSO: If already authenticated, only consent needed

**Phase 3: Token Exchange** (Standard PKCE)
- Client exchanges MCP authorization code
- Validates PKCE code_verifier
- Returns access token (aud: mcp-server)
- Client never sees master refresh token

### Implementation Status Section
- Document current implementation as "simplified hybrid flow"
- List what's implemented vs what needs refactoring
- Clarify current tests use simplified version
- Note progressive consent is target architecture

## Benefits of Progressive Consent

 Standards-compliant: Proper OAuth clients at IdP level
 Secure: Client validation against IdP registry
 Efficient: Nextcloud consent only once per user
 Transparent: Users understand each authorization step
 SSO-friendly: Minimal re-authentication in Phase 2

## Implementation Tracking

The refactoring from simplified hybrid flow to progressive consent will
be tracked in a separate issue. Current implementation demonstrates:
- MCP server can intercept OAuth callbacks
- Refresh tokens stored securely
- PKCE flow works end-to-end
- Tool execution succeeds

🤖 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 02:34:30 +01:00
parent babd60e08b
commit 0d45120470
+223 -61
View File
@@ -109,83 +109,180 @@ The IdP (Keycloak) is configured to:
- **Shared IdP**: Issues audience-specific tokens, supports token exchange/refresh
- **Nextcloud**: Validates tokens with `aud: "nextcloud"` for API access
### MCP Client Registration
**IMPORTANT**: MCP SDK clients (like Claude Desktop) are **proper OAuth clients registered at the IdP level**, not with the MCP server itself.
#### Client Registration Options
**Option 1: Dynamic Client Registration (DCR) - Recommended**
```python
# MCP client registers itself at IdP startup
import httpx
async def register_with_idp():
response = await httpx.post(
"https://idp.example.com/register",
json={
"client_name": "Claude Desktop",
"redirect_uris": ["http://localhost:51234/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none", # Public client with PKCE
"application_type": "native",
}
)
client_id = response.json()["client_id"]
# Store client_id for subsequent OAuth flows
```
**Option 2: Pre-registered Client**
```bash
# Admin pre-registers known MCP clients in Keycloak/Nextcloud
# Client IDs: "claude-desktop", "continue-dev", "zed-editor", etc.
```
**Key Points:**
- MCP clients are registered at **IdP level** (Nextcloud OIDC, Keycloak, Auth0, etc.)
- MCP server validates `client_id` against IdP registry during authorization
- Public clients use PKCE (no client_secret) per RFC 8252
- Each MCP client has its own identity and permissions
### Authentication Flows
#### Initial Setup with Hybrid Flow (One-Time)
#### Initial Setup with Progressive Consent (One-Time Per User)
This flow demonstrates **progressive consent**: separate authorization for MCP client authentication and Nextcloud resource access.
**Phase 1: MCP Client Authentication (Always Required)**
```mermaid
sequenceDiagram
participant User
participant MCPClient as MCP Client<br/>(Native App)
participant MCPClient as MCP Client<br/>(Claude Desktop)
participant MCPServer as MCP Server
participant IdP as Shared IdP (Keycloak)
participant Nextcloud
participant IdP as Shared IdP<br/>(Nextcloud OIDC)
User->>MCPClient: Connect to MCP
User->>MCPClient: Connect to MCP Server
MCPClient->>MCPServer: Initial request
MCPServer-->>MCPClient: 401 Unauthorized + OAuth config
MCPServer-->>MCPClient: 401 + OAuth endpoints
Note over MCPClient: Generate PKCE values:<br/>code_verifier = random string<br/>code_challenge = SHA256(code_verifier)
Note over MCPClient: Generate PKCE:<br/>verifier + challenge
MCPClient->>MCPClient: Start local HTTP server<br/>on random port (e.g., :51234)
MCPClient->>MCPClient: Start local callback server<br/>http://localhost:51234/callback
MCPClient->>MCPServer: GET /oauth/authorize<br/>+ code_challenge<br/>+ redirect_uri=http://localhost:51234/callback
MCPClient->>MCPServer: GET /oauth/authorize<br/>client_id="claude-desktop"<br/>redirect_uri=http://localhost:51234/callback<br/>code_challenge + S256
MCPServer->>MCPServer: Store session with:<br/>- client_redirect_uri<br/>- code_challenge<br/>- state
MCPServer->>MCPClient: 302 Redirect to IdP<br/>redirect_uri=https://mcp-server.com/oauth/callback
MCPServer->>IdP: Validate client_id exists<br/>(query registration endpoint)
IdP-->>MCPServer: Client valid ✓
Note over MCPServer,IdP: CRITICAL: Server's callback URL,<br/>NOT client's!
MCPServer->>MCPServer: Store session:<br/>- mcp_client_id="claude-desktop"<br/>- client_redirect_uri<br/>- code_challenge<br/>- consent_stage="client_auth"
MCPClient->>IdP: Authorization Request<br/>redirect_uri=https://mcp-server.com/oauth/callback
Note over IdP: Requested scopes:<br/>- openid profile email<br/>- offline_access<br/>- nextcloud:notes:*
MCPServer->>MCPClient: 302 Redirect to IdP<br/>client_id="claude-desktop" ← MCP client's ID!<br/>redirect_uri=https://mcp-server/oauth/callback<br/>scope=openid profile email
IdP->>User: Login page
User->>IdP: Authenticate once
Note over MCPServer,IdP: MCP Server intercepts with its own<br/>callback to manage token flow
IdP->>User: Consent screen
Note over IdP: "Allow MCP Server to:<br/>- Authenticate you<br/>- Access data offline<br/>- Access Nextcloud on your behalf"
MCPClient->>IdP: Follow redirect
IdP->>User: Login page (if not SSO)
User->>IdP: Authenticate
User->>IdP: Grant consent
IdP->>MCPServer: 302 Redirect to MCP server<br/>with IdP authorization code
IdP->>User: Consent: "Allow Claude Desktop<br/>to access MCP Server?"
User->>IdP: Approve
Note over MCPServer: Server receives IdP code!
IdP->>MCPServer: 302 to /oauth/callback<br/>code={client_auth_code}
MCPServer->>IdP: Exchange IdP code for tokens<br/>+ client_secret
IdP->>MCPServer: Master tokens:<br/>- Access token (aud: mcp-server)<br/>- Master refresh token
MCPServer->>IdP: POST /token<br/>code={client_auth_code}<br/>client_id="claude-desktop"<br/>redirect_uri=https://mcp-server/oauth/callback
MCPServer->>MCPServer: 1. Store master refresh token (encrypted)<br/>2. Generate MCP auth code: mcp-code-xyz<br/>3. Link to stored code_challenge
IdP->>MCPServer: Tokens:<br/>- access_token (aud: mcp-server)<br/>- id_token (user_id)
MCPServer->>MCPClient: 302 Redirect to client<br/>http://localhost:51234/callback<br/>?code=mcp-code-xyz&state=...
Note over MCPClient: Client receives MCP code<br/>(not IdP code!)
MCPClient->>MCPServer: POST /oauth/token<br/>code=mcp-code-xyz<br/>+ code_verifier
MCPServer->>MCPServer: 1. Find session by mcp-code-xyz<br/>2. Verify PKCE: SHA256(code_verifier) == code_challenge<br/>3. Get stored access token from step 4
MCPServer-->>MCPClient: Return:<br/>- Access token (aud: mcp-server)<br/>- NO master refresh token!<br/>- Optional: MCP session refresh token
MCPClient->>MCPServer: API call with token<br/>(aud: mcp-server)
MCPServer->>MCPServer: Validate audience
Note over MCPServer: Need Nextcloud access,<br/>use stored master refresh token
MCPServer->>IdP: POST /token<br/>refresh_token + audience=nextcloud
IdP->>MCPServer: New token (aud: nextcloud)
MCPServer->>Nextcloud: API call with token<br/>(aud: nextcloud)
Nextcloud->>IdP: Validate token + audience
IdP-->>Nextcloud: Valid for Nextcloud
Nextcloud-->>MCPServer: API response
MCPServer-->>MCPClient: Success
MCPServer->>MCPServer: Extract user_id from id_token<br/>Store MCP client access token
```
**Key Changes in the Hybrid Flow:**
1. **Server Intercepts Code**: The IdP redirects to the MCP server's `/oauth/callback`, not the client's
2. **Token Swap**: The server exchanges the IdP code for master tokens and stores them
3. **Client Handoff**: The server generates its own code (`mcp-code-xyz`) and redirects the client with it
4. **PKCE Completion**: The client exchanges the server's code using the original code_verifier
5. **Master Token Protection**: The client never receives the master refresh token
**Phase 2: Conditional Nextcloud Consent (Only If No Refresh Token)**
```mermaid
sequenceDiagram
participant User
participant MCPClient as MCP Client
participant MCPServer as MCP Server
participant TokenStore as Token Storage
participant IdP as Shared IdP
participant Nextcloud
MCPServer->>TokenStore: Check: Has refresh token<br/>for user_id?
alt Refresh Token EXISTS
Note over MCPServer: Skip Nextcloud consent ✓
MCPServer->>MCPServer: Generate mcp_auth_code
MCPServer->>MCPClient: 302 to client callback<br/>code=mcp-code-xyz
Note over MCPServer,MCPClient: Jump to Phase 3
else NO Refresh Token
MCPServer->>MCPServer: Update session:<br/>consent_stage="nextcloud_access"<br/>Store intermediate state
MCPServer->>MCPClient: 302 to IdP (SECOND OAuth!)<br/>client_id="nextcloud-mcp-server" ← MCP server's ID!<br/>redirect_uri=https://mcp-server/oauth/callback_nextcloud<br/>scope=openid offline_access notes:* calendar:*
MCPClient->>IdP: Follow redirect
Note over IdP: User may already be logged in (SSO)<br/>Only need consent, not re-auth
IdP->>User: Consent: "Allow MCP Server<br/>to access Nextcloud offline?"
User->>IdP: Approve offline_access
IdP->>MCPServer: 302 to /oauth/callback_nextcloud<br/>code={nextcloud_auth_code}
MCPServer->>IdP: POST /token<br/>code={nextcloud_auth_code}<br/>client_id="nextcloud-mcp-server"<br/>client_secret={mcp_server_secret}
IdP->>MCPServer: Tokens:<br/>- access_token (aud: nextcloud)<br/>- refresh_token ← MASTER TOKEN!
MCPServer->>TokenStore: Store refresh token<br/>(encrypted, user_id)
MCPServer->>MCPServer: Retrieve intermediate state<br/>Generate mcp_auth_code
MCPServer->>MCPClient: 302 to client callback<br/>code=mcp-code-xyz
end
```
**Phase 3: Complete MCP Client Flow (Standard PKCE)**
```mermaid
sequenceDiagram
participant MCPClient as MCP Client
participant MCPServer as MCP Server
participant TokenStore as Token Storage
MCPClient->>MCPClient: Callback received:<br/>code=mcp-code-xyz
MCPClient->>MCPServer: POST /oauth/token<br/>code=mcp-code-xyz<br/>code_verifier + PKCE<br/>client_id="claude-desktop"
MCPServer->>MCPServer: Validate PKCE:<br/>SHA256(verifier) == challenge
MCPServer->>TokenStore: Retrieve MCP client<br/>access token from Phase 1
MCPServer-->>MCPClient: Response:<br/>- access_token (aud: mcp-server)<br/>- token_type: Bearer<br/>- expires_in: 3600
Note over MCPClient: Client NEVER sees<br/>master refresh token!
MCPClient->>MCPServer: Connect MCP session<br/>Authorization: Bearer {token}
MCPServer->>MCPServer: Validate token audience
MCPServer->>MCPServer: For Nextcloud API calls,<br/>use stored refresh token
```
**Key Innovations in Progressive Consent:**
1. **Dual OAuth Flows**:
- Flow 1: Authenticate MCP client with IdP using client's own `client_id`
- Flow 2: Obtain Nextcloud permissions with MCP server's `client_id` (conditional)
2. **IdP-Level Client Validation**: MCP clients are registered at IdP, validated against registry
3. **Conditional Consent**: Nextcloud access only requested once per user, reused for subsequent sessions
4. **Token Isolation**:
- MCP client receives: `access_token` (aud: mcp-server)
- MCP server stores: `refresh_token` for Nextcloud access
- Complete separation of concerns
5. **SSO Efficiency**: If user authenticated in Phase 1, Phase 2 only requires consent (no re-login)
#### Subsequent MCP Sessions (Token Broker Pattern)
@@ -1225,15 +1322,34 @@ The Token Broker Architecture with **Hybrid Flow** and Audience Isolation provid
3. **Offline capabilities**: Master refresh tokens enable background operations
4. **Enterprise compliance**: Follows OAuth best practices and security standards
### Key Implementation: The Hybrid Flow
### Key Implementation: Progressive Consent with Dual OAuth Flows
The **Hybrid Flow** solves the critical problem of getting the master refresh token to the server while maintaining PKCE security for the client:
The **Progressive Consent architecture** (see "Initial Setup with Progressive Consent" above) solves the critical challenges of token brokering while maintaining standards compliance:
1. **Server Intercepts Code**: The IdP redirects to the MCP server's `/oauth/callback`, not the client's
2. **Server Gets Master Token**: The server exchanges the IdP code for the master refresh token and stores it
3. **Client Handoff**: The server generates its own authorization code and redirects the client
4. **PKCE Completion**: The client exchanges the server's code using the original PKCE verifier
5. **Token Protection**: The client never sees or handles the master refresh token
1. **MCP Client Authentication** (Phase 1):
- MCP clients are registered at IdP level (DCR or pre-configured)
- Client uses own `client_id` (e.g., "claude-desktop") for authentication
- MCP server validates client exists at IdP before proceeding
- User consents to "Claude Desktop accessing MCP Server"
2. **Conditional Nextcloud Consent** (Phase 2):
- Only triggered if MCP server doesn't have refresh token for user
- MCP server uses own `client_id` ("nextcloud-mcp-server") to request Nextcloud access
- User consents to "MCP Server accessing Nextcloud offline"
- MCP server stores master refresh token (encrypted)
3. **Token Exchange** (Phase 3):
- Standard PKCE flow between MCP client and MCP server
- Client exchanges MCP authorization code for access token
- Client never sees master refresh token
- Complete token isolation
**Benefits:**
- **Standards-compliant**: Proper OAuth 2.0 patterns throughout
- **Secure**: Client validation at IdP level, not local storage
- **Efficient**: Nextcloud consent only needed once per user
- **Transparent**: Users understand what they're authorizing at each step
- **SSO-friendly**: If authenticated in Phase 1, Phase 2 only requires consent
### Token Lifecycle Clarification
@@ -1243,9 +1359,55 @@ The **Hybrid Flow** solves the critical problem of getting the master refresh to
This architecture follows industry best practices for federated systems and positions the MCP server as a secure token broker in an enterprise identity ecosystem.
## Implementation Status
**Current Status**: Partially Implemented (Refactoring Required)
The current implementation (`nextcloud_mcp_server/auth/oauth_routes.py`) implements a **simplified hybrid flow** but needs refactoring to match the progressive consent architecture documented above:
### What's Currently Implemented ✅
1. **Basic OAuth endpoints**: `/oauth/authorize`, `/oauth/callback`, `/oauth/token`
2. **PKCE validation**: Code challenge/verifier flow works
3. **Session storage**: OAuth sessions stored in SQLite
4. **Token storage**: Master refresh tokens stored encrypted
5. **Integration tests**: Playwright-based tests pass (3/3)
### What Needs Refactoring 🔄
1. **Client Validation**:
- Current: `client_id` is ignored
- Needed: Validate `client_id` exists at IdP registry
2. **Dual OAuth Flow**:
- Current: Single OAuth using MCP server's `client_id`
- Needed: Phase 1 (MCP client auth) + Phase 2 (conditional Nextcloud consent)
3. **Consent Separation**:
- Current: Monolithic consent screen
- Needed: Separate consents for client authentication vs resource access
4. **Intermediate Session State**:
- Current: Simple session with MCP code generated upfront
- Needed: Store state between OAuth phases, support `consent_stage` field
5. **New Callback Endpoint**:
- Current: Single `/oauth/callback`
- Needed: Add `/oauth/callback_nextcloud` for Phase 2
### Migration Plan
The refactoring will be tracked in a separate issue. The current implementation serves as a proof-of-concept for the hybrid flow pattern and demonstrates:
- MCP server can intercept OAuth callbacks
- Refresh tokens can be securely stored
- MCP clients can connect using PKCE
- End-to-end tool execution works
The progressive consent architecture documented here represents the **target state** for production deployments.
## Testing
The ADR-004 Hybrid Flow is fully tested via automated integration tests:
The ADR-004 Hybrid Flow is currently tested via automated integration tests (using the simplified implementation):
### Integration Tests