feat: Complete ADR-004 Progressive Consent OAuth flows implementation

Implement dual OAuth flows for Progressive Consent architecture:

Flow 1 (Client Authentication):
- Client authenticates directly to IdP with its own client_id
- Server validates client_id against ALLOWED_MCP_CLIENTS whitelist
- Issues tokens with aud: "mcp-server" for MCP authentication only
- Progressive mode detected via ENABLE_PROGRESSIVE_CONSENT env var

Flow 2 (Resource Provisioning):
- New endpoints: /oauth/authorize-nextcloud, /oauth/callback-nextcloud
- MCP server acts as OAuth client for delegated Nextcloud access
- Stores master refresh tokens with flow_type and audience metadata
- Returns success HTML page after provisioning completion

Scope Authorization Updates:
- Added ProvisioningRequiredError for missing Flow 2 provisioning
- Decorator checks if Nextcloud scopes require provisioning in Progressive mode
- Validates token has Nextcloud scopes before allowing access

Storage Schema Enhancements:
- Added flow_type, is_provisioning, requested_scopes to oauth_sessions
- Enhanced store_oauth_session to support Progressive Consent metadata
- Maintains backward compatibility with hybrid flow

This completes the Progressive Consent implementation, enabling:
- Explicit user consent for resource access
- Stateless server by default (no automatic provisioning)
- Clear separation between authentication and resource access
- Defense in depth with audience-specific tokens

🤖 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 08:14:23 +01:00
co-authored by Claude
parent d16bcdcfbb
commit c896a2de63
3 changed files with 425 additions and 27 deletions
@@ -711,10 +711,14 @@ class RefreshTokenStorage:
code_challenge: Optional[str] = None,
code_challenge_method: Optional[str] = None,
mcp_authorization_code: Optional[str] = None,
client_id: Optional[str] = None,
flow_type: str = "hybrid",
is_provisioning: bool = False,
requested_scopes: Optional[str] = None,
ttl_seconds: int = 600, # 10 minutes
) -> None:
"""
Store OAuth session for Hybrid Flow (ADR-004).
Store OAuth session for ADR-004 Progressive Consent.
Args:
session_id: Unique session identifier
@@ -723,6 +727,10 @@ class RefreshTokenStorage:
code_challenge: PKCE code challenge
code_challenge_method: PKCE method (S256)
mcp_authorization_code: Pre-generated MCP authorization code
client_id: Client identifier (for Flow 1)
flow_type: Type of flow ('hybrid', 'flow1', 'flow2')
is_provisioning: Whether this is a Flow 2 provisioning session
requested_scopes: Requested OAuth scopes
ttl_seconds: Session TTL in seconds
"""
if not self._initialized:
@@ -735,17 +743,22 @@ class RefreshTokenStorage:
await db.execute(
"""
INSERT INTO oauth_sessions
(session_id, client_redirect_uri, state, code_challenge,
code_challenge_method, mcp_authorization_code, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
(session_id, client_id, client_redirect_uri, state, code_challenge,
code_challenge_method, mcp_authorization_code, flow_type,
is_provisioning, requested_scopes, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
session_id,
client_id,
client_redirect_uri,
state,
code_challenge,
code_challenge_method,
mcp_authorization_code,
flow_type,
is_provisioning,
requested_scopes,
now,
expires_at,
),