diff --git a/docker-compose.yml b/docker-compose.yml index 795c6ccb..5290cfdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -177,8 +177,10 @@ services: - VECTOR_SYNC_SCAN_INTERVAL=30 - VECTOR_SYNC_PROCESSOR_WORKERS=1 - # OAuth credentials for background sync (optional - uses DCR if not provided) - # Uncomment to avoid DCR: + # OAuth client for the MCP server's IdP registration. Falls back to DCR + # if unset — but with Nextcloud's built-in `oidc` app, DCR clients expire + # after ~1h (client_expire_time) and break auth permanently (see #907). + # For any non-CI deployment, register a static client and set these: # - NEXTCLOUD_OIDC_CLIENT_ID=your_client_id # - NEXTCLOUD_OIDC_CLIENT_SECRET=your_client_secret @@ -249,12 +251,6 @@ services: - TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:?TOKEN_ENCRYPTION_KEY must be set in .env (see env.sample)} - TOKEN_STORAGE_DB=/app/data/tokens.db - # ADR-005: Token exchange mode (RFC 8693) - # Exchange MCP tokens (aud: nextcloud-mcp-server) for Nextcloud tokens (aud: http://localhost:8080) - # Provides strict audience separation between MCP session and Nextcloud API access - - ENABLE_TOKEN_EXCHANGE=true - - TOKEN_EXCHANGE_CACHE_TTL=300 # Cache exchanged tokens for 5 minutes (default) - # Login Flow v2 (ADR-022) with external IdP — derived from the # auto-detected LOGIN_FLOW deployment mode; no separate flag needed. - ENABLE_DCR=true @@ -293,6 +289,14 @@ services: # the browser-based app-password layer is derived automatically. - MCP_DEPLOYMENT_MODE=login_flow + # NOTE: this dev/test service relies on Dynamic Client Registration for + # the MCP server's own OIDC client, which is fine for short-lived CI runs. + # For a real self-hosted login_flow deployment, register a STATIC client + # in Nextcloud (Administration → OpenID Connect provider) and set + # NEXTCLOUD_OIDC_CLIENT_ID / NEXTCLOUD_OIDC_CLIENT_SECRET — the built-in + # `oidc` app deletes DCR clients after ~1h, which breaks auth (see #907 + # and docs/login-flow-v2.md → Troubleshooting). + # Token storage (required for app password + session persistence). # Source the key from .env — see env.sample. To generate a fresh key: # python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" diff --git a/docs/ADR-002-vector-sync-authentication.md b/docs/ADR-002-vector-sync-authentication.md index bf2c70ee..95d9c7da 100644 --- a/docs/ADR-002-vector-sync-authentication.md +++ b/docs/ADR-002-vector-sync-authentication.md @@ -6,7 +6,7 @@ ## Status ~~Accepted - Tier 2 (Token Exchange with Delegation) Implemented~~ -**Superseded by ADR-004** - The token exchange implementation exists but doesn't solve the offline access problem. +**Superseded by ADR-004**, and ultimately by [ADR-022](ADR-022-deployment-mode-consolidation.md) (Login Flow v2) + [ADR-023](ADR-023-oauth-as-proxy.md) (OAuth AS proxy). The token-exchange approach was removed; background vector sync now uses Login Flow v2 app passwords. The only supported deployment modes are `single_user_basic`, `multi_user_basic`, and `login_flow`. **Important**: Service account tokens (old Tier 1) have been rejected as they violate OAuth "act on-behalf-of" principles by creating Nextcloud user accounts for the MCP server. diff --git a/docs/ADR-004-Code-Review.md b/docs/ADR-004-Code-Review.md deleted file mode 100644 index dc59b960..00000000 --- a/docs/ADR-004-Code-Review.md +++ /dev/null @@ -1,65 +0,0 @@ -Excellent and incredibly thorough work on ADR-004. It outlines a robust, secure, and modern approach to federated authentication that aligns with industry best practices. The Progressive Consent architecture with dual OAuth flows is the right direction for a system with these requirements. - -Here is a review of the current implementation in light of the architecture proposed in the ADR. - -### High-Level Assessment - -The project is in a good state, with a clear vision for its authentication architecture. The current implementation provides a backward-compatible "Hybrid Flow" while also containing the scaffolding for the target "Progressive Consent" flow. The hybrid flow is well-tested, which is a great foundation. - -The following points are intended to help bridge the gap between the current implementation and the final vision outlined in ADR-004. - -### Critical Security Review - -#### 1. Missing Token Audience (`aud`) Validation - -This is the most critical issue. The `require_scopes` decorator currently checks for scopes but does not validate the `audience` (`aud` claim) of the incoming JWT. - -* **Risk:** This creates a "confused deputy" vulnerability. An access token issued for a different application could be used to access the MCP server, as long as the scope names happen to match. -* **ADR Reference:** The ADR correctly identifies this and proposes an `MCPTokenVerifier` that validates `aud: "mcp-server"`. -* **Recommendation:** Implement the audience validation as a central part of your token verification middleware. An incoming token should be rejected immediately if its audience is not `mcp-server`. This check should happen before any tool-specific scope checks. - -### Architecture and Implementation Review - -#### 2. Progressive Consent Flow is Untested - -The code for the Progressive Consent flow (behind the `ENABLE_PROGRESSIVE_CONSENT` flag) exists in `oauth_routes.py` and `oauth_tools.py`. However, there are no integration tests to validate it. - -* **Risk:** Given the complexity of OAuth flows, it's likely there are bugs in the untested implementation. -* **Recommendation:** Create a new test file, `test_adr004_progressive_flow.py`, that uses Playwright to test the dual-flow architecture end-to-end: - 1. **Flow 1:** A test MCP client authenticates directly with the IdP to get an `mcp-server` token. - 2. **Provisioning Check:** The test verifies that calling a Nextcloud tool fails with a `ProvisioningRequiredError`. - 3. **Flow 2:** The test calls the `provision_nextcloud_access` tool and automates the second OAuth flow to grant the server offline access. - 4. **Tool Execution:** The test verifies that Nextcloud tools can now be successfully called. - -#### 3. Inconsistent Authorization URL Generation - -There is duplicated and inconsistent logic for generating the IdP authorization URL. - -* **Location 1:** `oauth_tools.py` in `generate_oauth_url_for_flow2` hardcodes the authorization endpoint path. -* **Location 2:** `oauth_routes.py` in `oauth_authorize_nextcloud` correctly uses the OIDC discovery document to find the `authorization_endpoint`. -* **Risk:** The hardcoded path is brittle and will break with IdPs that use different endpoint paths (like Keycloak). -* **Recommendation:** Consolidate this logic. The `provision_nextcloud_access` tool should not build the URL itself. Instead, it should return a URL pointing to the MCP server's own `/oauth/authorize-nextcloud` endpoint. This endpoint (which you've already created as `oauth_authorize_nextcloud` in `oauth_routes.py`) can then be the single source of truth for generating the IdP redirect. - -#### 4. Poor User Experience due to Missing Token Refresh - -The `/oauth/token` endpoint does not implement the `refresh_token` grant type. This means that when the client's `mcp-server` access token expires (e.g., after one hour), the user must go through the entire browser-based login flow again. - -* **Risk:** This creates a frustrating user experience, especially for long-lived desktop clients. -* **ADR Reference:** A proper Flow 1 should result in the MCP client receiving both an access token and a refresh token from the IdP. -* **Recommendation:** - 1. Ensure the IdP is configured to issue refresh tokens to the MCP client for Flow 1. - 2. The MCP client should securely store this refresh token. - 3. The client should use the refresh token to get new `mcp-server` access tokens directly from the IdP, without involving the MCP server or the user. The MCP server should not be involved in the client's session management with the IdP. - -### Summary - -The project is on the right track. The ADR is a solid plan, and the initial implementation is a good starting point. - -My recommendations in order of priority are: - -1. **Implement Audience Validation** to close the security gap. -2. **Add Integration Tests** for the Progressive Consent flow. -3. **Refactor the client-side token refresh** to improve user experience. -4. **Consolidate the URL generation** logic to fix the inconsistency. - -Addressing these points will align the implementation with the excellent vision in ADR-004 and result in a secure, robust, and user-friendly system. \ No newline at end of file diff --git a/docs/ADR-004-mcp-application-oauth.md b/docs/ADR-004-mcp-application-oauth.md index 92e372a5..74183ea2 100644 --- a/docs/ADR-004-mcp-application-oauth.md +++ b/docs/ADR-004-mcp-application-oauth.md @@ -1,6 +1,16 @@ # ADR-004: Federated Authentication Architecture for Offline Access -**Status**: Draft +> **⚠️ DEPRECATED / superseded.** The token-exchange and dual-flow "federated +> authentication" design described here was **not adopted**. The MCP server's +> auth was consolidated in [ADR-022](ADR-022-deployment-mode-consolidation.md) +> (Login Flow v2) and [ADR-023](ADR-023-oauth-as-proxy.md) (OAuth AS proxy). +> The only supported deployment modes are **`single_user_basic`**, +> **`multi_user_basic`**, and **`login_flow`** — there is no token-exchange mode. +> The durable ideas from this ADR (MCP server as its own OAuth client; token +> **audience** validation) live on via [ADR-005](ADR-005-token-audience-validation.md) +> and ADR-023. + +**Status**: Superseded by ADR-022 and ADR-023 (token-exchange design not adopted) **Date**: 2025-11-02 **Supersedes**: ADR-002 @@ -1529,11 +1539,16 @@ The **Progressive Consent architecture** solves the critical challenges of token 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 +## Historical Implementation Notes -**Current Status**: Partially Implemented (Refactoring Required) +> These were the planned refactoring steps at the time this ADR was written. +> They were **never completed** — the design was superseded by ADR-022 (Login +> Flow v2) and ADR-023 (OAuth AS proxy) before this progressive-consent / +> token-exchange architecture was adopted. The `ENABLE_TOKEN_EXCHANGE` / +> `settings.enable_token_exchange` symbols referenced below no longer exist in +> the codebase. Retained for historical context only. -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: +The implementation at the time (`nextcloud_mcp_server/auth/oauth_routes.py`) was a **simplified hybrid flow** that would have needed refactoring to match the progressive consent architecture documented above: ### What's Currently Implemented ✅ diff --git a/docs/ADR-005-token-audience-validation.md b/docs/ADR-005-token-audience-validation.md index 3e6c4d6c..3330403e 100644 --- a/docs/ADR-005-token-audience-validation.md +++ b/docs/ADR-005-token-audience-validation.md @@ -14,6 +14,13 @@ This ADR has been fully implemented with key simplifications based on RFC 7519 S - Clients discover resource via PRM endpoint (RFC 9728) - Nextcloud OIDC app uses client-specific resource URLs +> **Note:** The **token-exchange mode** (Option 2 / `ENABLE_TOKEN_EXCHANGE`) +> described in the sections below was **removed** in the ADR-022 (Login Flow v2) +> / ADR-023 (OAuth AS proxy) consolidation. Only **multi-audience mode** ships; +> `ENABLE_TOKEN_EXCHANGE` / `settings.enable_token_exchange` no longer exist. +> The token-exchange references in this document are retained for historical +> context only. + ## Executive Summary This ADR addresses a critical security vulnerability where the MCP server was passing tokens intended for itself directly to Nextcloud APIs (token passthrough). We will: diff --git a/docs/ADR-007-background-vector-sync-job-management.md b/docs/ADR-007-background-vector-sync-job-management.md index 0e7f817b..285320df 100644 --- a/docs/ADR-007-background-vector-sync-job-management.md +++ b/docs/ADR-007-background-vector-sync-job-management.md @@ -1,6 +1,6 @@ # ADR-007: Background Vector Database Synchronization -**Status**: Proposed +**Status**: Accepted — implemented (background vector sync ships; see `nc_get_vector_sync_status` and `VECTOR_SYNC_*` settings) **Date**: 2025-01-08 **Supersedes**: ADR-003 **Depends On**: ADR-004 (Federated Authentication), ADR-006 (Progressive Consent) diff --git a/docs/ADR-008-mcp-sampling-for-semantic-search.md b/docs/ADR-008-mcp-sampling-for-semantic-search.md index ecbf5529..958e0e11 100644 --- a/docs/ADR-008-mcp-sampling-for-semantic-search.md +++ b/docs/ADR-008-mcp-sampling-for-semantic-search.md @@ -1,6 +1,6 @@ # ADR-008: MCP Sampling for Multi-App Semantic Search with RAG -**Status**: Proposed +**Status**: Accepted — implemented (`nc_notes_semantic_search_answer` uses MCP sampling via `ctx.session.create_message`) **Date**: 2025-01-11 **Depends On**: ADR-007 (Background Vector Sync) diff --git a/docs/ADR-009-semantic-search-oauth-scope.md b/docs/ADR-009-semantic-search-oauth-scope.md index 34fd9630..d03c5bd1 100644 --- a/docs/ADR-009-semantic-search-oauth-scope.md +++ b/docs/ADR-009-semantic-search-oauth-scope.md @@ -1,6 +1,6 @@ # ADR-009: Generic `semantic:read` OAuth Scope for Multi-App Vector Search -**Status**: Proposed +**Status**: Accepted — implemented (`semantic.read` scope gates the semantic-search tools) **Date**: 2025-01-11 **Depends On**: ADR-007 (Background Vector Sync), ADR-008 (MCP Sampling for Semantic Search) diff --git a/docs/ADR-010-webhook-based-vector-sync.md b/docs/ADR-010-webhook-based-vector-sync.md index cf8c91ba..921c3248 100644 --- a/docs/ADR-010-webhook-based-vector-sync.md +++ b/docs/ADR-010-webhook-based-vector-sync.md @@ -1,6 +1,6 @@ # ADR-010: Webhook-Based Vector Database Synchronization -**Status**: Proposed +**Status**: Accepted — implemented (webhook listener registration; see `auth/webhook_routes.py` and the `registered_webhooks` store) **Date**: 2025-01-10 **Depends On**: ADR-007 (Background Vector Sync) diff --git a/docs/ADR-012-unified-multi-algorithm-search.md b/docs/ADR-012-unified-multi-algorithm-search.md index 9acd85af..40547c57 100644 --- a/docs/ADR-012-unified-multi-algorithm-search.md +++ b/docs/ADR-012-unified-multi-algorithm-search.md @@ -1,7 +1,7 @@ # ADR-012: Unified Multi-Algorithm Search with Client-Configurable Weighting ## Status -Proposed +Accepted — implemented (hybrid search with RRF/DBSF fusion; see `search/hybrid.py`) ## Context diff --git a/docs/ADR-013-rag-evaluation.md b/docs/ADR-013-rag-evaluation.md index dfe140ff..977368bc 100644 --- a/docs/ADR-013-rag-evaluation.md +++ b/docs/ADR-013-rag-evaluation.md @@ -1,6 +1,6 @@ ## ADR-013: RAG Evaluation Testing Framework -**Status:** Proposed +**Status:** Partially implemented (RAG evaluation harness lives under `tests/rag_evaluation`) **Date:** 2025-11-15 diff --git a/docs/ADR-018-nextcloud-php-app-for-settings-ui.md b/docs/ADR-018-nextcloud-php-app-for-settings-ui.md index 59602548..decb7a8c 100644 --- a/docs/ADR-018-nextcloud-php-app-for-settings-ui.md +++ b/docs/ADR-018-nextcloud-php-app-for-settings-ui.md @@ -1,6 +1,6 @@ # ADR-018: Nextcloud PHP App for Settings and Management UI -**Status**: Proposed +**Status**: Accepted — implemented (the Astrolabe Nextcloud app provides the settings/management UI) **Date**: 2025-12-14 **Updated**: 2025-12-15 (Added deployment modes and authentication architecture) **Related**: ADR-011 (AppAPI Architecture - Rejected), ADR-008 (MCP Sampling), ADR-004 (OAuth Progressive Consent) diff --git a/docs/ADR-025-dynaconf-configuration-management.md b/docs/ADR-025-dynaconf-configuration-management.md index 33defa6c..748dfa61 100644 --- a/docs/ADR-025-dynaconf-configuration-management.md +++ b/docs/ADR-025-dynaconf-configuration-management.md @@ -1,6 +1,6 @@ -# ADR-024: Dynaconf Configuration Management +# ADR-025: Dynaconf Configuration Management -**Status:** Proposed +**Status:** Accepted — implemented (`config.py` is built on dynaconf) **Date:** 2026-04-04 **Deciders:** Development Team **Related:** ADR-020 (Deployment Modes), ADR-021 (Configuration Consolidation), ADR-022 (Login Flow v2) diff --git a/docs/CRITICAL-TOKEN-EXCHANGE-PATTERN.md b/docs/CRITICAL-TOKEN-EXCHANGE-PATTERN.md deleted file mode 100644 index c3e59d21..00000000 --- a/docs/CRITICAL-TOKEN-EXCHANGE-PATTERN.md +++ /dev/null @@ -1,348 +0,0 @@ -# Token Acquisition Patterns for ADR-004 Progressive Consent - -## Overview - -ADR-004 Progressive Consent establishes the authorization architecture (Flow 1 for client auth, Flow 2 for resource provisioning). This document describes **how tokens are acquired for different operational contexts** within that architecture. - -**Key Principle**: Refresh tokens from Flow 2 (Progressive Consent) should **NEVER** be used for MCP tool calls - they are exclusively for background jobs. - -## Implementation Status - -**Current Status**: ✅ Token exchange infrastructure implemented, available as opt-in feature - -The MCP server supports two token acquisition modes: -1. **Pass-through mode** (default, `ENABLE_TOKEN_EXCHANGE=false`): Simple, stateless -2. **Token exchange mode** (opt-in, `ENABLE_TOKEN_EXCHANGE=true`): Enhanced security with token delegation - -Both modes maintain the critical separation: **refresh tokens are never used for tool calls**. - -## Current Default (Pass-Through Mode) - -### What Happens (ENABLE_TOKEN_EXCHANGE=false): -1. Client gets Flow 1 token (`aud: "mcp-server"`) -2. Client calls MCP tool -3. Server validates Flow 1 token -4. Server passes Flow 1 token to Nextcloud -5. Nextcloud validates token with IdP -6. Refresh tokens (from Flow 2) used **only** for background jobs - -### Characteristics: -- ✅ Simple, stateless operation -- ✅ Clear separation: Flow 1 tokens for sessions, refresh tokens for background -- ✅ Lower latency (no token exchange round-trip) -- ✅ Works with any OAuth IdP - -## Optional Token Exchange Mode - -### Token Exchange Pattern (ENABLE_TOKEN_EXCHANGE=true) - -**MCP Session (Foreground Operations)**: - -``` -┌─────────────┐ Flow 1 Token ┌──────────────┐ -│ MCP Client │ ───(aud: mcp-server)──> │ MCP Server │ -└─────────────┘ └──────────────┘ - │ - Tool Call │ - "search_notes()" │ - ▼ - ┌─────────────────────┐ - │ Token Exchange │ - │ 1. Validate Flow 1 │ - │ 2. Check permission │ - │ 3. Request delegated│ - │ Nextcloud token │ - └─────────────────────┘ - │ - │ Exchange Request - ▼ - ┌─────────────────────┐ - │ IdP Token Endpoint │ - │ (Token Exchange) │ - └─────────────────────┘ - │ - │ Delegated Token - │ (aud: nextcloud) - │ (limited scopes) - │ (short-lived) - ▼ - ┌─────────────────────┐ - │ Nextcloud API Call │ - │ GET /notes │ - └─────────────────────┘ -``` - -**Key Properties of Session Tokens:** -- ✅ Generated **on-demand** during tool execution -- ✅ **Ephemeral** - used only for current operation -- ✅ **NOT stored** - discarded after use -- ✅ **Limited scopes** - only what tool needs (e.g., `notes.read` for search) -- ✅ **Short-lived** - expires quickly (e.g., 5 minutes) - -**Background Jobs (Offline Operations)**: - -``` -┌─────────────────┐ Scheduled Job ┌──────────────┐ -│ Background │ ──────────────────────> │ Worker │ -│ Scheduler │ │ Process │ -└─────────────────┘ └──────────────┘ - │ - │ Use stored - │ refresh token - ▼ - ┌─────────────────────┐ - │ Refresh Token Store │ - │ (Flow 2 provisioned)│ - └─────────────────────┘ - │ - │ Refresh Token - ▼ - ┌─────────────────────┐ - │ IdP Token Endpoint │ - │ (Refresh Grant) │ - └─────────────────────┘ - │ - │ Background Token - │ (aud: nextcloud) - │ (different scopes) - │ (longer-lived) - ▼ - ┌─────────────────────┐ - │ Nextcloud API │ - │ (Background Sync) │ - └─────────────────────┘ -``` - -**Key Properties of Background Tokens:** -- ✅ Obtained from **stored refresh token** (Flow 2) -- ✅ **Different scopes** than session tokens (e.g., `notes:sync`, `files:sync`) -- ✅ **Longer-lived** for background operations -- ✅ **Never used for MCP sessions** -- ✅ **Only for offline/background jobs** - -## Implementation Requirements - -### 1. Token Exchange Endpoint - -Implement RFC 8693 Token Exchange: - -```python -# nextcloud_mcp_server/auth/token_exchange.py - -async def exchange_token_for_delegation( - flow1_token: str, - requested_audience: str = "nextcloud", - requested_scopes: list[str] | None = None -) -> tuple[str, int]: - """ - Exchange Flow 1 MCP token for delegated Nextcloud token. - - This implements RFC 8693 Token Exchange for on-behalf-of delegation. - - IMPORTANT: Nextcloud doesn't support OAuth scopes natively. Scopes are - soft-scopes enforced by the MCP server via @require_scopes decorator, - not by the IdP or Nextcloud. Therefore, requested_scopes are not passed - to the IdP during token exchange. - - Args: - flow1_token: The MCP session token (aud: "mcp-server") - requested_audience: Target audience (usually "nextcloud") - requested_scopes: Ignored (Nextcloud doesn't support scopes) - - Returns: - Tuple of (delegated_token, expires_in) - """ - # 1. Validate Flow 1 token (audience check) - # 2. Check user has provisioned Nextcloud access (Flow 2) - # 3. Request token exchange from IdP (without scopes - Nextcloud doesn't support them) - # 4. Return ephemeral delegated token -``` - -### 2. Unified get_client() Pattern - -The token acquisition mode is handled transparently by `get_client()`: - -```python -# nextcloud_mcp_server/context.py - -async def get_client(ctx: Context) -> NextcloudClient: - """ - Get the appropriate Nextcloud client based on authentication mode. - - This function handles three modes: - 1. BasicAuth mode: Returns shared client from lifespan context - 2. OAuth pass-through mode (ENABLE_TOKEN_EXCHANGE=false, default): - Verifies Flow 1 token and passes it to Nextcloud - 3. OAuth token exchange mode (ENABLE_TOKEN_EXCHANGE=true): - Exchanges Flow 1 token for ephemeral Nextcloud token via RFC 8693 - """ - 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"): - # Check if token exchange is enabled - if settings.enable_token_exchange: - # Token exchange mode: Exchange Flow 1 token for ephemeral Nextcloud token - return await get_session_client_from_context( - ctx, lifespan_ctx.nextcloud_host - ) - else: - # Pass-through mode (default): Verify and pass Flow 1 token to Nextcloud - return get_client_from_context(ctx, lifespan_ctx.nextcloud_host) -``` - -### 3. MCP Tool Pattern (No Changes Required!) - -Tools use the same pattern regardless of token acquisition mode: - -```python -@mcp.tool() -@require_scopes("notes.read") # Soft-scope enforced by MCP server, not Nextcloud -@require_provisioning -async def nc_notes_search_notes(query: str, ctx: Context) -> SearchNotesResponse: - """Search notes by title or content.""" - - # get_client() handles both pass-through and token exchange modes - client = await get_client(ctx) - - # Execute operation - results = await client.notes.search_notes(query=query) - - # In token exchange mode, ephemeral token is automatically discarded - # In pass-through mode, Flow 1 token was validated and passed through - return SearchNotesResponse(results=results) -``` - -**Key Benefit**: Tools don't need to know which mode is active. The token acquisition pattern is configured at the server level via `ENABLE_TOKEN_EXCHANGE`. - -### 4. Background Job Pattern - -Background jobs use a **different token acquisition pattern** - they use refresh tokens from Flow 2: - -```python -# Background worker -async def sync_notes_job(user_id: str): - """Background job to sync notes.""" - - # Get refresh token stored during Flow 2 (Progressive Consent) - token_storage = get_token_storage() - refresh_token = await token_storage.get_refresh_token(user_id) - - if not refresh_token: - logger.warning(f"No refresh token for user {user_id}") - return - - # Use refresh token to get Nextcloud access token - idp_client = get_idp_client() - response = await idp_client.refresh_token( - refresh_token=refresh_token, - audience='nextcloud' - ) - - # Create client with background token (can be cached) - client = NextcloudClient.from_token( - base_url=NEXTCLOUD_HOST, - token=response.access_token, - username=user_id - ) - - # Perform background sync - await client.notes.sync_all() -``` - -**Key differences from tool calls:** -- Uses refresh tokens from Flow 2 (Progressive Consent provisioning) -- Tokens can be cached for efficiency (longer-lived operations) -- No user interaction possible (offline) -- Never triggered during MCP tool execution - -## Security Benefits - -### Proper Token Exchange: -1. ✅ **Least Privilege**: Each operation gets only needed scopes -2. ✅ **Time-Limited**: Session tokens expire quickly -3. ✅ **Audit Trail**: Each exchange can be logged -4. ✅ **Token Isolation**: Session ≠ Background tokens -5. ✅ **Revocation**: Can revoke background access without affecting active sessions - -### Current Incorrect Pattern: -1. ❌ **Over-Privileged**: Refresh token has all scopes -2. ❌ **Long-Lived**: Same token reused indefinitely -3. ❌ **No Separation**: Sessions and background jobs use same credential -4. ❌ **Revocation Issues**: Revoking affects everything - -## Implementation Steps - -### Phase 1: Token Exchange (High Priority) -1. Implement RFC 8693 token exchange endpoint -2. Update Token Broker with `get_session_token()` vs `get_background_token()` -3. Modify tool pattern to use token exchange - -### Phase 2: Scope Separation (High Priority) -1. Define session scopes vs background scopes -2. Update provisioning flow to request appropriate scopes -3. Validate scopes in token exchange - -### Phase 3: Background Jobs (Medium Priority) -1. Implement background worker pattern -2. Create scheduled jobs (note sync, etc.) -3. Use background token pattern - -### Phase 4: Testing (High Priority) -1. Test token exchange flow end-to-end -2. Verify session tokens are ephemeral -3. Verify background tokens are separate -4. Load test token exchange performance - -## References - -- **RFC 8693**: OAuth 2.0 Token Exchange -- **RFC 9068**: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens -- **ADR-004**: Progressive Consent OAuth Flows -- **OAuth 2.0 Delegation**: On-Behalf-Of vs Impersonation patterns - -## Status - -**Current Status**: ✅ Token exchange infrastructure implemented, available as opt-in feature -**Modes Available**: -- ✅ Pass-through mode (default, `ENABLE_TOKEN_EXCHANGE=false`): Simple, stateless -- ✅ Token exchange mode (opt-in, `ENABLE_TOKEN_EXCHANGE=true`): Enhanced security - -**Implementation Complete**: -- ✅ `token_exchange.py` module with RFC 8693 support -- ✅ Fallback to refresh grant when RFC 8693 not supported -- ✅ `get_client()` unified pattern (handles both modes transparently) -- ✅ Tokens never cached in token exchange mode (ephemeral) -- ✅ Background jobs use separate pattern (refresh tokens from Flow 2) - -## Configuration - -To enable token exchange mode: - -```bash -# docker-compose.yml or .env -ENABLE_TOKEN_EXCHANGE=true -``` - -When enabled, all MCP tool calls will use token exchange (RFC 8693) to obtain ephemeral Nextcloud tokens. When disabled (default), Flow 1 tokens are passed through to Nextcloud. - -## Nextcloud Scope Limitation - -**IMPORTANT**: Nextcloud does not support OAuth scopes natively. Scopes like "notes.read" are **soft-scopes** enforced by the MCP server via `@require_scopes` decorator, not by the IdP or Nextcloud. - -This means: -- Token exchange provides audit and delegation benefits, not scope restriction -- All Nextcloud tokens have equivalent permissions at the Nextcloud level -- Fine-grained access control is enforced by MCP server, not Nextcloud - -## Next Actions (Optional Enhancements) - -1. [ ] Add integration tests for token exchange mode with actual MCP tools -2. [ ] Document background job patterns for scheduled sync operations -3. [ ] Add metrics for token exchange performance -4. [ ] Consider making token exchange the default in future major version diff --git a/docs/configuration-migration-v2.md b/docs/configuration-migration-v2.md index 3aac3740..db40b66c 100644 --- a/docs/configuration-migration-v2.md +++ b/docs/configuration-migration-v2.md @@ -27,8 +27,11 @@ This guide helps you migrate from the old configuration variables to the new con |----------|----------|--------| | `VECTOR_SYNC_ENABLED` | `ENABLE_SEMANTIC_SEARCH` | Deprecated | | `ENABLE_OFFLINE_ACCESS` | `ENABLE_BACKGROUND_OPERATIONS` | Deprecated | +| `ENABLE_TOKEN_EXCHANGE` | `MCP_DEPLOYMENT_MODE=login_flow` | Removed (now ignored) | | N/A (auto-detected) | `MCP_DEPLOYMENT_MODE` | New (optional) | +> **`ENABLE_TOKEN_EXCHANGE` was removed.** The OAuth token-exchange mode was never fully implemented and was retired in the ADR-022/ADR-023 consolidation. The variable is now silently ignored — use `MCP_DEPLOYMENT_MODE=login_flow` for multi-user OAuth. + **Tuning parameters unchanged:** - `VECTOR_SYNC_SCAN_INTERVAL` - Keep as-is - `VECTOR_SYNC_PROCESSOR_WORKERS` - Keep as-is @@ -233,54 +236,6 @@ NEXTCLOUD_OIDC_CLIENT_SECRET=secret --- -### Scenario 5: Token Exchange Mode with Semantic Search - -**Before (v0.57.x):** -```bash -NEXTCLOUD_HOST=https://nextcloud.example.com -ENABLE_TOKEN_EXCHANGE=true - -# Both required -ENABLE_OFFLINE_ACCESS=true -VECTOR_SYNC_ENABLED=true - -TOKEN_ENCRYPTION_KEY=your-key-here -TOKEN_STORAGE_DB=/app/data/tokens.db -TOKEN_EXCHANGE_CACHE_TTL=300 -QDRANT_URL=http://qdrant:6333 -OLLAMA_BASE_URL=http://ollama:11434 -``` - -**After (v0.58.0+ - Simplified):** -```bash -NEXTCLOUD_HOST=https://nextcloud.example.com -ENABLE_TOKEN_EXCHANGE=true - -# Optional: Explicit mode declaration -MCP_DEPLOYMENT_MODE=oauth_token_exchange - -# One variable! -ENABLE_SEMANTIC_SEARCH=true # Auto-enables background operations - -TOKEN_ENCRYPTION_KEY=your-key-here -TOKEN_STORAGE_DB=/app/data/tokens.db -TOKEN_EXCHANGE_CACHE_TTL=300 -QDRANT_URL=http://qdrant:6333 -OLLAMA_BASE_URL=http://ollama:11434 -``` - -**What Changed:** -- ✅ Semantic search auto-enables background operations -- ✅ Explicit mode declaration available - -**Migration Steps:** -1. Replace `VECTOR_SYNC_ENABLED=true` with `ENABLE_SEMANTIC_SEARCH=true` -2. Remove `ENABLE_OFFLINE_ACCESS=true` (auto-enabled) -3. Optionally add `MCP_DEPLOYMENT_MODE=oauth_token_exchange` -4. Restart server - ---- - ## Understanding Automatic Dependency Resolution ### How It Works @@ -500,8 +455,7 @@ We provide mode-specific templates for new deployments: | Template | Use Case | |----------|----------| | `env.sample.single-user` | Simplest setup | -| `env.sample.oauth-multi-user` | Recommended multi-user | -| `env.sample.oauth-advanced` | Token exchange mode | +| `env.sample.oauth-multi-user` | Recommended multi-user (Login Flow v2) | **Usage:** ```bash diff --git a/docs/configuration.md b/docs/configuration.md index cc928444..0e6add60 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -19,7 +19,7 @@ cp env.sample .env # Full reference with all options # Edit .env with your Nextcloud details ``` -> **Note:** The legacy templates `env.sample.oauth-multi-user` and `env.sample.oauth-advanced` configure the deprecated direct-OAuth-to-Nextcloud modes. New deployments should use [Login Flow v2](login-flow-v2.md) for multi-user setups. +> **Note:** `env.sample.oauth-multi-user` is a Login Flow v2 quick-start template for multi-user setups. See [Login Flow v2](login-flow-v2.md). Then choose your deployment mode: @@ -97,6 +97,13 @@ MCP_DEPLOYMENT_MODE=login_flow TOKEN_ENCRYPTION_KEY= TOKEN_STORAGE_DB=/app/data/tokens.db +# Static OIDC client for the MCP server's own IdP registration. +# Strongly recommended — with Nextcloud's built-in oidc app the DCR +# fallback expires after ~1h (see the warning below). Create the client +# under Administration settings → OpenID Connect provider. +NEXTCLOUD_OIDC_CLIENT_ID= +NEXTCLOUD_OIDC_CLIENT_SECRET= + # Public URLs for browser redirects NEXTCLOUD_MCP_SERVER_URL=https://mcp.example.com NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.instance.com @@ -110,10 +117,22 @@ NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.instance.com | `TOKEN_STORAGE_DB` | ✅ Yes | Path to SQLite DB for stored app passwords (use a persistent volume) | | `NEXTCLOUD_MCP_SERVER_URL` | ✅ Yes | Public URL of the MCP server (used as the audience claim and for browser redirects) | | `NEXTCLOUD_PUBLIC_ISSUER_URL` | ✅ Yes | Public URL of Nextcloud (for browser redirects during Login Flow v2) | -| `NEXTCLOUD_OIDC_CLIENT_ID` | ⚠️ Optional (preferred) | OIDC client ID for the MCP server's relying-party registration with the IdP (Nextcloud OIDC by default; Keycloak / Cognito / etc. via `OIDC_DISCOVERY_URL`). If unset and the IdP advertises a `registration_endpoint`, RFC 7591 DCR is used as fallback. | -| `NEXTCLOUD_OIDC_CLIENT_SECRET` | ⚠️ Optional (preferred) | OIDC client secret paired with `NEXTCLOUD_OIDC_CLIENT_ID`. | +| `NEXTCLOUD_OIDC_CLIENT_ID` | ✅ Strongly recommended | OIDC client ID for the MCP server's relying-party registration with the IdP (Nextcloud's built-in OIDC by default; Keycloak / Cognito / etc. via `OIDC_DISCOVERY_URL`). If unset and the IdP advertises a `registration_endpoint`, the server falls back to RFC 7591 Dynamic Client Registration (DCR) — **but with Nextcloud's built-in `oidc` app this fallback breaks after ~1 hour** (see warning below). Create a static client and set this instead. | +| `NEXTCLOUD_OIDC_CLIENT_SECRET` | ✅ Strongly recommended | OIDC client secret paired with `NEXTCLOUD_OIDC_CLIENT_ID`. | | `OIDC_DISCOVERY_URL` | Optional | Override the IdP discovery URL. Defaults to `${NEXTCLOUD_HOST}/.well-known/openid-configuration` (Nextcloud's built-in OIDC). Set to a Keycloak realm or AWS Cognito user-pool discovery URL to use an external IdP. | +> **⚠️ Use a static OIDC client with Nextcloud's built-in `oidc` app.** If you +> don't set `NEXTCLOUD_OIDC_CLIENT_ID` / `NEXTCLOUD_OIDC_CLIENT_SECRET`, the MCP +> server registers its own relying-party client via DCR. Nextcloud's `oidc` app +> treats DCR clients as **ephemeral** and deletes them after `client_expire_time` +> (default **3600s = 1 hour**), pruning on every `/authorize`. Once it's gone, +> authorization and token refresh fail and users hit an **"Access forbidden"** +> page — permanently, because the server keeps reusing the deleted client. +> Register a permanent client in **Administration settings → OpenID Connect +> provider** and set the two env vars. See +> [Login Flow v2 → Troubleshooting](login-flow-v2.md#troubleshooting) and +> [issue #907](https://github.com/cbcoutinho/nextcloud-mcp-server/issues/907). + See [Login Flow v2](login-flow-v2.md) for full setup, scope reference, and troubleshooting. --- diff --git a/docs/login-flow-v2.md b/docs/login-flow-v2.md index 7d7e74e8..fa7042b9 100644 --- a/docs/login-flow-v2.md +++ b/docs/login-flow-v2.md @@ -52,9 +52,12 @@ NEXTCLOUD_HOST=https://your.nextcloud.example.com # OIDC client credentials for the MCP server's relying-party relationship with the IdP. # These are generic OIDC client credentials — they work with any OIDC provider, despite -# the Nextcloud-flavored env-var names. Preferred path: register a client in your IdP -# (Nextcloud admin → OIDC, Keycloak realm → Clients, etc.) and set these. If both are -# unset and the IdP advertises a `registration_endpoint`, the server falls back to RFC 7591 DCR. +# the Nextcloud-flavored env-var names. Register a static client in your IdP +# (Nextcloud admin → OpenID Connect provider, Keycloak realm → Clients, etc.) and set these. +# +# Strongly recommended — do NOT rely on the DCR fallback with Nextcloud's built-in +# `oidc` app: it deletes dynamically-registered clients after ~1h, which breaks the +# connection permanently (see Troubleshooting → "Access forbidden" below). NEXTCLOUD_OIDC_CLIENT_ID= NEXTCLOUD_OIDC_CLIENT_SECRET= @@ -63,7 +66,7 @@ MCP_DEPLOYMENT_MODE=login_flow # App-password storage (required for persistence across restarts) TOKEN_STORAGE_DB=/app/data/tokens.db -TOKEN_ENCRYPTION_KEY= # see "Generating an encryption key" below +TOKEN_ENCRYPTION_KEY= # see "Generating an encryption key" below # Public URLs (for browser redirects) NEXTCLOUD_MCP_SERVER_URL=https://mcp.example.com @@ -72,6 +75,23 @@ NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.example.com # Public URL of When using an external IdP (Keycloak, Cognito, etc.), see [Keycloak Multi-Client Token Validation](keycloak-multi-client-validation.md) for how Nextcloud's `user_oidc` app handles realm-level token validation if you also federate Nextcloud's own login through the same IdP. +### Default IdP setup (Nextcloud's built-in `oidc` app) + +When `OIDC_DISCOVERY_URL` is unset, Nextcloud's own **OpenID Connect provider** +(`oidc`) app is the IdP. Register a **static** client for the MCP server there — +don't rely on Dynamic Client Registration, because the `oidc` app auto-deletes +DCR clients after ~1 hour (see [Troubleshooting](#access-forbidden-after-the-connection-worked-for-a-while)). + +1. Install/enable the **OpenID Connect provider** (`oidc`) app. +2. Go to **Administration settings → OpenID Connect provider → Add client** and set: + - **Redirect URI:** `https:///oauth/callback` + - **Flow / response type:** authorization **code** + - **Type:** **confidential** (so it issues a client secret) + - **Resource identifier:** `https:///mcp` (so issued tokens carry the MCP server's audience; the verifier's `_has_mcp_audience` accepts both this `/mcp` form and the bare server URL) + - **Scopes:** leave empty to allow all, or list the per-app scopes you want plus `openid profile email offline_access` +3. Copy the generated client ID and secret into `NEXTCLOUD_OIDC_CLIENT_ID` / + `NEXTCLOUD_OIDC_CLIENT_SECRET`. + ### External IdP setup (Authentik / Keycloak / Cognito) When `OIDC_DISCOVERY_URL` points at a third-party IdP rather than Nextcloud's own @@ -173,9 +193,14 @@ mcp-login-flow: - NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004 - NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080 - MCP_DEPLOYMENT_MODE=login_flow + # Production: register a static OIDC client and set these — the DCR + # fallback used by this dev/test service expires after ~1h against the + # built-in `oidc` app (see "Default IdP setup" above and #907). + # - NEXTCLOUD_OIDC_CLIENT_ID= + # - NEXTCLOUD_OIDC_CLIENT_SECRET= # Dev-only inline value. In production, mount via Docker secret and read # from a *_FILE env var or a secrets-management init step. - - TOKEN_ENCRYPTION_KEY= + - TOKEN_ENCRYPTION_KEY= - TOKEN_STORAGE_DB=/app/data/tokens.db volumes: - login-flow-data:/app/data @@ -329,6 +354,33 @@ JWTs are preferred for production because validation is local and stateless. Opa ## Troubleshooting +### "Access forbidden" after the connection worked for a while + +**Symptom:** authentication succeeds and tools work for a while (often up to an +hour), then the connection silently drops. Re-connecting redirects to Nextcloud +and shows an **"Access forbidden"** page. Restarting the MCP server and +re-creating the MCP client/connector don't help. ([#907](https://github.com/cbcoutinho/nextcloud-mcp-server/issues/907)) + +**Cause:** you didn't set `NEXTCLOUD_OIDC_CLIENT_ID` / `NEXTCLOUD_OIDC_CLIENT_SECRET`, +so the MCP server registered *its own* relying-party client with Nextcloud's +built-in `oidc` app via Dynamic Client Registration (DCR). The `oidc` app treats +DCR clients as ephemeral and **deletes them after `client_expire_time` (default +3600s = 1 hour)** — it prunes expired DCR clients on every `/authorize` request. +Once the server's client is gone, `/authorize` can't find it (→ the "Access +forbidden" page) and token refresh fails too. It's permanent because the server +cached that now-deleted client in `tokens.db` and keeps reusing it. + +**Fix:** register a **static** (admin-created, non-DCR) client and configure it — +see [Default IdP setup](#default-idp-setup-nextclouds-built-in-oidc-app). Static +clients are never auto-deleted. Set `NEXTCLOUD_OIDC_CLIENT_ID` / +`NEXTCLOUD_OIDC_CLIENT_SECRET` (they take precedence over the cached DCR client) +and recreate the container. Existing users will need to re-authorize once after +this switch — their stored sessions were issued to the now-deleted DCR client, +so old refresh tokens no longer validate against the new static client. + +As a non-recommended stopgap you can extend the DCR client lifetime globally: +`occ config:app:set oidc client_expire_time --value 31536000`. + ### "Provisioning loop" — user keeps being asked to authorize Check that `TOKEN_STORAGE_DB` is on a persistent volume. The default (`/tmp` or per-process tempfile) is wiped on container restart, so each restart loses every stored app password. diff --git a/docs/oauth-architecture-comparison.md b/docs/oauth-architecture-comparison.md deleted file mode 100644 index 65db9eb6..00000000 --- a/docs/oauth-architecture-comparison.md +++ /dev/null @@ -1,323 +0,0 @@ -# OAuth Architecture Comparison: MCP Server Authentication Patterns - -This document compares three authentication architectures for the MCP server, explaining the evolution from pass-through authentication to true offline access capabilities. - -## Pattern 1: Pass-Through Authentication (Current Implementation) - -### Architecture -``` -┌─────────────┐ OAuth Flow ┌─────────────┐ -│ MCP Client │◄──────────────────│ OAuth │ -│ (Claude) │ │ Provider │ -└──────┬──────┘ └─────────────┘ - │ - │ Access Token - │ (per request) - ▼ -┌─────────────┐ ┌─────────────┐ -│ MCP Server │───────────────────►│ Nextcloud │ -│(Pass-through) │ APIs │ -└─────────────┘ └─────────────┘ -``` - -### Characteristics -| Aspect | Description | -|--------|-------------| -| **Token Flow** | MCP Client → MCP Server → Nextcloud | -| **Token Storage** | None (tokens exist only during request) | -| **Offline Access** | ❌ Impossible | -| **Background Workers** | ❌ Not supported | -| **User Consent** | Single OAuth flow (client-managed) | -| **Complexity** | Low | -| **Security** | High (no token persistence) | - -### How It Works -1. MCP Client performs OAuth with provider -2. Client includes access token in each MCP request -3. MCP Server validates token and forwards to Nextcloud -4. Token discarded after request completes - -### Limitations -- No operations possible without active MCP session -- Background sync/indexing impossible -- Cannot refresh tokens independently - ---- - -## Pattern 2: Token Exchange Delegation (ADR-002 - Flawed) - -### Architecture -``` -┌─────────────┐ ┌─────────────┐ -│ MCP Client │────────────────────│ OAuth │ -│ (Claude) │ │ Provider │ -└──────┬──────┘ └──────┬──────┘ - │ │ - │ Access Token │ Service Account Token - ▼ ▼ -┌─────────────────────────────────────────────┐ -│ MCP Server │ -│ ┌────────────────────────────────────┐ │ -│ │ Token Exchange (RFC 8693) │ │ -│ │ Subject: Service Account │ │ -│ │ Target: User │ │ -│ └────────────────────────────────────┘ │ -└───────────────┬─────────────────────────────┘ - │ Exchanged Token - ▼ - ┌─────────────┐ - │ Nextcloud │ - │ APIs │ - └─────────────┘ -``` - -### Characteristics -| Aspect | Description | -|--------|-------------| -| **Token Flow** | Service Account → Exchange → User Token | -| **Token Storage** | None (MCP server still stateless) | -| **Offline Access** | ❌ Still impossible (circular dependency) | -| **Background Workers** | ❌ Requires service account (rejected) | -| **User Consent** | Implicit through service account | -| **Complexity** | High | -| **Security** | ⚠️ Service accounts violate OAuth principles | - -### Why It Fails -1. **Circular Dependency**: To exchange tokens, you need a token to exchange -2. **Service Account Problem**: Creates Nextcloud user identity for service -3. **OAuth Violation**: Service acts as itself, not on behalf of users -4. **No Bootstrap**: Still can't obtain initial tokens offline - -### The Fatal Flaw -``` -Q: How does background worker get tokens? -A: Use token exchange with service account - -Q: How does service account get authorized? -A: Client credentials grant creates user account (violates OAuth) - -Q: Can we use user's refresh token? -A: MCP server never sees refresh tokens (by design) -``` - ---- - -## Pattern 3: Sign-in with Nextcloud (Previous ADR-004 Draft) - -### Architecture -``` -┌─────────────┐ ┌─────────────────┐ ┌────────────┐ -│ MCP Client ├───────────────────> │ MCP Server ├────────────────────>│ Nextcloud │ -│ (Claude) │ (MCP Protocol) │ (OAuth Client) │ (OIDC + APIs) │ (IdP) │ -└─────────────┘ └─────────────────┘ └────────────┘ - │ - ┌──────▼────────┐ - │ Token Storage │ - │ (NC Tokens) │ - └───────────────┘ -``` - -### Characteristics -| Aspect | Description | -|--------|-------------| -| **Token Flow** | MCP Server uses Nextcloud as identity provider | -| **Token Storage** | ✅ Encrypted Nextcloud refresh tokens | -| **Offline Access** | ✅ Full support | -| **Background Workers** | ✅ Use stored refresh tokens | -| **User Consent** | Single OAuth flow (Nextcloud only) | -| **Complexity** | Medium | -| **Security** | High (with token rotation) | - -### How It Works -1. **Initial Setup**: - - User tries to use MCP tool - - MCP server returns auth required - - User authenticates with Nextcloud's OIDC endpoint - - Nextcloud may use user_oidc to delegate to external IdP (Keycloak, etc.) - - MCP server stores Nextcloud-issued refresh token (encrypted) - -2. **Subsequent Requests**: - - MCP server uses stored Nextcloud tokens - - Refreshes automatically when expired - - No client involvement needed - -3. **Background Operations**: - - Worker retrieves stored refresh token - - Refreshes with Nextcloud directly - - Performs operations independently - -### Advantages -- ✅ Single sign-on with Nextcloud -- ✅ True offline access capability -- ✅ OAuth-compliant with proper consent -- ✅ Supports external IdPs via user_oidc -- ✅ Simpler integration - only one OAuth endpoint - -### Trade-offs -- Authentication flows through Nextcloud -- Nextcloud manages IdP relationships (via user_oidc) -- MCP server only knows about Nextcloud, not the underlying IdP - ---- - -## Pattern 4: Federated Authentication Architecture (ADR-004 - Solution) - -### Architecture -``` -┌─────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌────────────┐ -│ MCP Client │◄──────401──────│ MCP Server │◄────OAuth──────│ Shared IdP │──Validates──►│ Nextcloud │ -│ (Claude) │ │ (OAuth Client) │ (On-Behalf) │ (Keycloak) │ Tokens │(Resource) │ -└─────────────┘ └─────────────────┘ └──────────────┘ └────────────┘ - │ - ┌───────▼────────┐ - │ Token Storage │ - │ (IdP Tokens) │ - └────────────────┘ -``` - -### Characteristics -| Aspect | Description | -|--------|-------------| -| **Token Flow** | Shared IdP issues tokens for Nextcloud access | -| **Token Storage** | ✅ Encrypted IdP refresh tokens | -| **Offline Access** | ✅ Full support | -| **Background Workers** | ✅ Use stored IdP refresh tokens | -| **User Consent** | Single OAuth flow (IdP manages consent) | -| **Complexity** | Medium-High | -| **Security** | Highest (enterprise-grade IdP) | - -### How It Works -1. **Initial Setup**: - - MCP client connects, receives 401 - - Browser opens MCP server OAuth URL - - MCP server redirects to shared IdP - - User authenticates once to IdP - - IdP shows consent for both identity and Nextcloud access - - MCP server stores IdP refresh token (encrypted) - - MCP server issues session token to client - -2. **Subsequent Requests**: - - MCP server validates session token - - Uses stored IdP token for Nextcloud - - Refreshes with IdP when expired - - No client involvement needed - -3. **Background Operations**: - - Worker retrieves stored IdP refresh token - - Gets new access token from IdP - - Uses token to access Nextcloud - - Performs operations independently - -### Advantages -- ✅ True single sign-on (SSO) -- ✅ Enterprise-ready with SAML/LDAP support -- ✅ OAuth-compliant with proper delegation -- ✅ Direct IdP relationship - no intermediary -- ✅ Flexible - can swap resource servers -- ✅ Industry-standard federated pattern - -### Trade-offs -- Requires shared IdP infrastructure -- More complex initial setup -- Token validation overhead - ---- - -## Comparison Matrix - -| Feature | Pass-Through | Token Exchange | Sign-in with NC | Federated Auth | -|---------|--------------|----------------|-----------------|----------------| -| **Offline Access** | ❌ No | ❌ No | ✅ Yes | ✅ Yes | -| **Background Workers** | ❌ No | ❌ No* | ✅ Yes | ✅ Yes | -| **Token Storage** | None | None | NC refresh tokens | IdP refresh tokens | -| **OAuth Compliance** | ✅ Full | ⚠️ Violates | ✅ Full | ✅ Full | -| **User Consent** | Once | Implicit | Once (NC) | Once (IdP) | -| **Implementation Complexity** | Low | High | Medium | Medium-High | -| **Security** | High | Medium | High | Highest | -| **Enterprise Ready** | ❌ No | ❌ No | ⚠️ Indirect | ✅ Yes | -| **Identity Provider** | Client-managed | N/A | Nextcloud (+user_oidc) | Shared IdP | -| **Suitable For** | Interactive only | N/A (flawed) | Small teams | Enterprise | - -\* *Requires service accounts that violate OAuth principles* - ---- - -## Evolution Summary - -### Stage 1: Simple Pass-Through ✅ -- **Goal**: Basic MCP functionality -- **Result**: Works well for interactive use -- **Limitation**: No offline capabilities - -### Stage 2: Attempted Delegation ❌ -- **Goal**: Enable offline access without changing architecture -- **Result**: Circular dependencies, OAuth violations -- **Learning**: MCP protocol constraints are fundamental - -### Stage 3: Sign-in with Nextcloud ⚠️ -- **Goal**: True offline access with OAuth compliance -- **Result**: MCP server uses Nextcloud as identity provider -- **Limitation**: Tight coupling to Nextcloud, no enterprise IdP - -### Stage 4: Federated Pattern ✅ -- **Goal**: Enterprise-ready offline access -- **Result**: Shared IdP for both MCP server and Nextcloud -- **Trade-off**: Additional infrastructure justified by enterprise needs - ---- - -## Key Insights - -1. **Pattern 3 vs Pattern 4**: Both support external IdPs, but differ in integration approach: - - Pattern 3: MCP → Nextcloud OIDC → (user_oidc) → External IdP - - Pattern 4: MCP → External IdP directly (Nextcloud also uses same IdP) - - Choose Pattern 3 for Nextcloud-centric deployments, Pattern 4 for IdP-centric enterprises - -2. **The MCP Protocol Boundary**: The MCP protocol creates a fundamental boundary between client and server token management. Attempting to breach this boundary (ADR-002) leads to architectural contradictions. - -3. **Service Accounts Don't Solve User Problems**: Using service accounts for user operations violates OAuth's core principle of acting on behalf of users, not as a service identity. - -4. **Double OAuth is Industry Standard**: Major platforms (Zapier, IFTTT, Microsoft Power Automate) use this pattern - the integration platform is an OAuth client that maintains its own relationships with upstream services. - -5. **Refresh Tokens Are The Solution**: The OAuth spec designed refresh tokens specifically for offline access. Rejecting them (as ADR-002 did) means rejecting the standard solution. - -6. **Complexity is Justified**: The additional complexity of managing OAuth flows is acceptable when offline access is a requirement. The alternative is no offline access at all. - ---- - -## Recommendations - -### For Simple Deployments -Use **Pattern 1 (Pass-Through)** if: -- Offline access not needed -- Only interactive operations required -- Simplicity is priority - -### For Teams Using Nextcloud -Use **Pattern 3 (Sign-in with Nextcloud)** if: -- Background sync/indexing required -- Nextcloud manages your authentication -- Can use external IdPs via user_oidc -- Prefer single integration point through Nextcloud - -### For Enterprise Deployments -Use **Pattern 4 (Federated Authentication)** if: -- Enterprise IdP already exists (Keycloak, Okta, Azure AD) -- Multiple resource servers beyond Nextcloud -- Compliance requirements for centralized auth -- Building platform for multiple organizations - -### Never Use Pattern 2 -Token Exchange with service accounts should not be used as it: -- Doesn't enable true offline access -- Violates OAuth principles -- Adds complexity without solving the problem - ---- - -## References - -- [ADR-002: Vector Database Background Sync Authentication (Deprecated)](./ADR-002-vector-sync-authentication.md) -- [ADR-004: MCP Server as OAuth Client for Offline Access](./ADR-004-mcp-application-oauth.md) -- [RFC 6749: OAuth 2.0 Framework](https://datatracker.ietf.org/doc/html/rfc6749) -- [RFC 8693: OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693) \ No newline at end of file diff --git a/docs/observability.md b/docs/observability.md index 03b18453..73528963 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -72,7 +72,6 @@ The Helm chart has moved to a [separate repository](https://github.com/cbcoutinh ### OAuth Flow Metrics - `mcp_oauth_token_validations_total` - Token validation count -- `mcp_oauth_token_exchange_total` - Token exchange operations - `mcp_oauth_token_cache_hits_total` - Cache hit/miss rate - `mcp_oauth_refresh_token_operations_total` - Refresh token storage ops diff --git a/docs/webhook-management-guide.md b/docs/webhook-management-guide.md index c0e55003..cdc2cb9e 100644 --- a/docs/webhook-management-guide.md +++ b/docs/webhook-management-guide.md @@ -207,24 +207,7 @@ php occ webhook_listeners:remove --- -### 4. OAuth Token Exchange (RFC 8693) - -**Configuration:** -```bash -NEXTCLOUD_HOST=http://nextcloud.example.com -ENABLE_TOKEN_EXCHANGE=true -ENABLE_BACKGROUND_OPERATIONS=true -TOKEN_ENCRYPTION_KEY= -TOKEN_STORAGE_DB=/app/data/tokens.db -VECTOR_SYNC_ENABLED=true -``` - -**Enable/Disable Webhooks:** -Same process as OAuth Single-Audience. The token exchange happens transparently when the MCP server accesses Nextcloud APIs. - ---- - -### 5. Smithery Stateless +### 4. Smithery Stateless **Configuration:** - Configuration from session URL params diff --git a/env.sample b/env.sample index e845e94d..906ab3f2 100644 --- a/env.sample +++ b/env.sample @@ -101,40 +101,6 @@ NEXTCLOUD_PASSWORD= # Optional features (semantic search, document processing): # See "Optional Features" section below -# ============================================ -# OAUTH TOKEN EXCHANGE MODE (Advanced) -# ============================================ -# Multi-user OAuth with RFC 8693 token exchange -# Use for: Advanced deployments requiring separate MCP and Nextcloud tokens -# MCP tokens are separate from Nextcloud tokens -# -# Required: -#ENABLE_TOKEN_EXCHANGE=true -# -# Optional - Pre-registered OAuth Client: -# If you pre-register the client instead of using DCR: -#NEXTCLOUD_OIDC_CLIENT_ID= -#NEXTCLOUD_OIDC_CLIENT_SECRET= -# -# Optional - Token Exchange Configuration: -# Cache TTL in seconds (default: 300 = 5 minutes) -#TOKEN_EXCHANGE_CACHE_TTL=300 -# -# Optional - Background Operations: -# Note: ENABLE_SEMANTIC_SEARCH automatically enables this in multi-user modes -#ENABLE_BACKGROUND_OPERATIONS=true -#TOKEN_ENCRYPTION_KEY= -#TOKEN_STORAGE_DB=/app/data/tokens.db -# -# Optional - Custom OIDC Discovery: -#NEXTCLOUD_OIDC_DISCOVERY_URL= -# -# MCP Server URL (for OAuth redirects): -#NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000 -# -# Optional features (semantic search, document processing): -# See "Optional Features" section below - # ============================================ # OAUTH CLIENT ALLOWLISTS (OAuth modes) # ============================================ diff --git a/env.sample.oauth-advanced b/env.sample.oauth-advanced deleted file mode 100644 index ca805590..00000000 --- a/env.sample.oauth-advanced +++ /dev/null @@ -1,80 +0,0 @@ -# ============================================ -# OAUTH TOKEN EXCHANGE QUICK START (Advanced) -# ============================================ -# Advanced OAuth deployment with RFC 8693 token exchange -# Use for: Deployments requiring separate MCP and Nextcloud tokens -# Features: Dual-audience tokens, enhanced security boundaries -# -# Copy this file to .env and configure - -# ===== REQUIRED SETTINGS ===== -# Your Nextcloud instance URL (without trailing slash) -NEXTCLOUD_HOST=https://nextcloud.example.com - -# Enable token exchange mode -ENABLE_TOKEN_EXCHANGE=true - -# ===== REQUIRED: LEAVE USERNAME/PASSWORD EMPTY ===== -# OAuth mode activates when these are NOT set -NEXTCLOUD_USERNAME= -NEXTCLOUD_PASSWORD= - -# ===== OPTIONAL: EXPLICIT MODE DECLARATION ===== -# Recommended for clarity -MCP_DEPLOYMENT_MODE=oauth_token_exchange - -# ===== OPTIONAL: PRE-REGISTERED OAUTH CLIENT ===== -# If you pre-register the OAuth client instead of using DCR: -#NEXTCLOUD_OIDC_CLIENT_ID=your-client-id -#NEXTCLOUD_OIDC_CLIENT_SECRET=your-client-secret - -# MCP Server URL (for OAuth redirects) -NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000 - -# ===== OPTIONAL: TOKEN EXCHANGE TUNING ===== -# Cache TTL for exchanged tokens (default: 300 seconds = 5 minutes) -TOKEN_EXCHANGE_CACHE_TTL=300 - -# ===== OPTIONAL: SEMANTIC SEARCH ===== -# AI-powered semantic search with automatic background operation setup -# -# Note: ENABLE_SEMANTIC_SEARCH automatically enables background operations -# in token exchange mode, just like in OAuth single-audience mode -# -ENABLE_SEMANTIC_SEARCH=true - -# Vector Database (required for semantic search) -QDRANT_URL=http://qdrant:6333 - -# Embedding Provider (required for semantic search) -OLLAMA_BASE_URL=http://ollama:11434 -OLLAMA_EMBEDDING_MODEL=nomic-embed-text - -# Token Storage (required for background operations - auto-enabled by semantic search) -# Generate encryption key: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" -TOKEN_ENCRYPTION_KEY=your-encryption-key-here -TOKEN_STORAGE_DB=/app/data/tokens.db - -# ===== OPTIONAL: DOCUMENT PROCESSING ===== -# Extract text from PDFs, images, DOCX for semantic search -#ENABLE_DOCUMENT_PROCESSING=true -#ENABLE_UNSTRUCTURED=true -#UNSTRUCTURED_API_URL=http://unstructured:8000 - -# ===== TOKEN EXCHANGE MODE EXPLANATION ===== -# In this mode: -# 1. MCP clients authenticate with tokens scoped to "mcp-server" audience -# 2. Server exchanges MCP tokens for Nextcloud tokens on each request -# 3. Provides clear separation between MCP session and Nextcloud access -# 4. Enables fine-grained token lifecycle management -# -# When to use: -# - Strict security requirements (separate token contexts) -# - Complex multi-service architectures -# - Need independent token expiration policies -# -# When NOT to use: -# - Simple deployments (use oauth_single_audience instead) -# - High-performance requirements (token exchange adds latency) - -# For more configuration options, see env.sample diff --git a/env.sample.oauth-multi-user b/env.sample.oauth-multi-user index f61ad186..f21c4bdb 100644 --- a/env.sample.oauth-multi-user +++ b/env.sample.oauth-multi-user @@ -1,10 +1,12 @@ # ============================================ -# OAUTH MULTI-USER QUICK START (Recommended) +# LOGIN FLOW v2 MULTI-USER QUICK START (Recommended) # ============================================ -# Multi-user deployment with OAuth authentication -# Use for: Multi-user production deployments, enhanced security -# Features: Single-audience tokens, automatic client registration (DCR) +# Multi-user deployment with OAuth/OIDC authentication (ADR-022). +# Use for: Multi-user production deployments, enhanced security. +# The MCP server authenticates clients via OIDC and holds per-user +# Nextcloud app passwords (encrypted) obtained via Login Flow v2. # +# See docs/login-flow-v2.md for the full guide. # Copy this file to .env and configure # ===== REQUIRED SETTINGS ===== @@ -16,17 +18,32 @@ NEXTCLOUD_HOST=https://nextcloud.example.com NEXTCLOUD_USERNAME= NEXTCLOUD_PASSWORD= -# ===== OPTIONAL: EXPLICIT MODE DECLARATION ===== -# Recommended for clarity -MCP_DEPLOYMENT_MODE=oauth_single_audience +# ===== REQUIRED: DEPLOYMENT MODE ===== +MCP_DEPLOYMENT_MODE=login_flow -# ===== OPTIONAL: PRE-REGISTERED OAUTH CLIENT ===== -# If you pre-register the OAuth client instead of using DCR: -#NEXTCLOUD_OIDC_CLIENT_ID=your-client-id -#NEXTCLOUD_OIDC_CLIENT_SECRET=your-client-secret +# ===== STRONGLY RECOMMENDED: STATIC OIDC CLIENT ===== +# Register a static client for the MCP server in your IdP and set these. +# With Nextcloud's built-in `oidc` app you MUST do this: the DCR fallback +# registers an ephemeral client that the app deletes after ~1h, which breaks +# auth permanently ("Access forbidden" on reconnect — see issue #907). +# Create one under Administration settings -> OpenID Connect provider. +NEXTCLOUD_OIDC_CLIENT_ID= +NEXTCLOUD_OIDC_CLIENT_SECRET= -# MCP Server URL (for OAuth redirects) +# ===== REQUIRED: PUBLIC URLs ===== +# Public URL of the MCP server (used as the token audience and for OAuth redirects). NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000 +# Public URL of Nextcloud as the user's browser sees it (for Login Flow v2 +# browser redirects). Omitting it causes the "Login URL points to localhost" +# failure — see docs/login-flow-v2.md#troubleshooting. +NEXTCLOUD_PUBLIC_ISSUER_URL=https://nextcloud.example.com + +# ===== REQUIRED: APP-PASSWORD STORAGE ===== +# Required for login_flow — per-user Nextcloud app passwords are stored here +# (encrypted) so they survive restarts. See docs/login-flow-v2.md#setup. +# Generate encryption key: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +TOKEN_ENCRYPTION_KEY= +TOKEN_STORAGE_DB=/app/data/tokens.db # ===== OPTIONAL: SEMANTIC SEARCH (Recommended) ===== # AI-powered semantic search with automatic background operation setup @@ -53,11 +70,6 @@ OLLAMA_EMBEDDING_MODEL=nomic-embed-text #AWS_REGION=us-east-1 #BEDROCK_EMBEDDING_MODEL=amazon.titan-embed-text-v2:0 -# Token Storage (required for background operations - auto-enabled by semantic search) -# Generate encryption key: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" -TOKEN_ENCRYPTION_KEY=your-encryption-key-here -TOKEN_STORAGE_DB=/app/data/tokens.db - # ===== OPTIONAL: DOCUMENT PROCESSING ===== # Extract text from PDFs, images, DOCX for semantic search #ENABLE_DOCUMENT_PROCESSING=true @@ -68,10 +80,11 @@ TOKEN_STORAGE_DB=/app/data/tokens.db # With ENABLE_SEMANTIC_SEARCH=true in OAuth mode: # ✅ Background operations enabled automatically # ✅ Refresh token storage enabled automatically -# ✅ OAuth credentials required (DCR or pre-registered) +# ✅ Static OIDC client credentials required (see above) # ✅ Encryption key required for token storage # # You only need to set ENABLE_SEMANTIC_SEARCH and provide the required -# infrastructure (Qdrant, Ollama, encryption key). The rest is automatic! +# infrastructure (static OIDC client, Qdrant, Ollama, encryption key). +# The rest is automatic! # For more advanced configuration, see env.sample diff --git a/nextcloud_mcp_server/auth/unified_verifier.py b/nextcloud_mcp_server/auth/unified_verifier.py index bbdade87..8dc83b2a 100644 --- a/nextcloud_mcp_server/auth/unified_verifier.py +++ b/nextcloud_mcp_server/auth/unified_verifier.py @@ -2,15 +2,12 @@ Unified Token Verifier for ADR-005 Token Audience Validation. This module replaces both NextcloudTokenVerifier and ProgressiveConsentTokenVerifier -with a single implementation that supports two compliant OAuth modes: - -1. Multi-audience mode (default): Validates MCP audience per RFC 7519 (resource servers - validate only their own audience). Nextcloud independently validates its own audience. -2. Token exchange mode (opt-in): Tokens have MCP audience only, exchanged for Nextcloud tokens +with a single implementation using multi-audience validation: it validates the MCP +audience per RFC 7519 (resource servers validate only their own audience), and +Nextcloud independently validates its own audience when it receives the token. Key Design Principles: - Token verification happens HERE (validates MCP audience per OAuth spec) -- Token exchange happens in context_helper.py (when creating NextcloudClient) - No token passthrough allowed (complies with MCP Security Specification) - Token reuse IS allowed for multi-audience tokens (RFC 8707) """ @@ -39,18 +36,14 @@ logger = logging.getLogger(__name__) class UnifiedTokenVerifier(TokenVerifier): """ - Unified token verifier supporting both multi-audience and token exchange modes. + Unified token verifier for multi-audience tokens (ADR-005). Compliant with MCP security specification - no token pass-through. This verifier: 1. Validates tokens using JWT verification with JWKS or introspection fallback - 2. Enforces proper audience validation based on configured mode + 2. Enforces MCP audience validation (per RFC 7519); Nextcloud independently + validates its own audience when receiving API calls 3. Caches successful validations to avoid repeated API calls - - Mode Selection (via ENABLE_TOKEN_EXCHANGE setting): - - False/omit (default): Multi-audience mode - validates MCP audience only (per RFC 7519). - Nextcloud independently validates its own audience when receiving API calls. - - True: Exchange mode - requires MCP audience only, then exchanges for Nextcloud token """ def __init__(self, settings: Settings): @@ -61,7 +54,6 @@ class UnifiedTokenVerifier(TokenVerifier): settings: Application settings containing OAuth configuration """ self.settings = settings - self.mode = "multi-audience" # Common components for all modes self.http_client = nextcloud_httpx_client(timeout=10.0) @@ -118,8 +110,7 @@ class UnifiedTokenVerifier(TokenVerifier): ) logger.info( - "UnifiedTokenVerifier initialized in %s mode. MCP audience: %s or %s, Nextcloud resource URI: %s, Valid issuers: %s", - self.mode, + "UnifiedTokenVerifier initialized (multi-audience). MCP audience: %s or %s, Nextcloud resource URI: %s, Valid issuers: %s", settings.oidc_client_id, settings.nextcloud_mcp_server_url, settings.nextcloud_resource_uri, @@ -130,10 +121,9 @@ class UnifiedTokenVerifier(TokenVerifier): """ Verify token according to MCP TokenVerifier protocol. - Per RFC 7519, we validate only MCP audience. The mode determines what - happens AFTER verification in context_helper.py: - - Multi-audience mode: Use token directly (Nextcloud validates its own audience) - - Exchange mode: Exchange for Nextcloud-audience token via RFC 8693 + Per RFC 7519, we validate only MCP audience. The token is then used + directly against Nextcloud (which validates its own audience) — see + context_helper.py. Args: token: Bearer token to verify @@ -150,7 +140,6 @@ class UnifiedTokenVerifier(TokenVerifier): oauth_token_cache_hits_total.labels(hit="false").inc() - # Both modes do the same validation (MCP audience only) return await self._verify_mcp_audience(token) async def verify_token_for_management_api(self, token: str) -> AccessToken | None: @@ -292,16 +281,10 @@ class UnifiedTokenVerifier(TokenVerifier): record_oauth_token_validation(validation_method, "invalid") return None - # Log based on mode for clarity - if self.mode == "multi-audience": - logger.info( - "MCP audience validated - token can be used directly " - "(Nextcloud will validate its own audience)" - ) - else: - logger.info( - "MCP audience validated - token will be exchanged for Nextcloud access" - ) + logger.info( + "MCP audience validated - token can be used directly " + "(Nextcloud will validate its own audience)" + ) return self._create_access_token(token, payload) diff --git a/nextcloud_mcp_server/config.py b/nextcloud_mcp_server/config.py index d99af469..d97d508e 100644 --- a/nextcloud_mcp_server/config.py +++ b/nextcloud_mcp_server/config.py @@ -57,7 +57,6 @@ _DEFAULTS: dict[str, Any] = { "enable_background_operations": False, "vector_sync_enabled": False, "enable_offline_access": False, - "enable_token_exchange": False, # Token storage "token_encryption_key": None, # None = ephemeral per-process tempfile (see get_token_db_path()). @@ -1268,7 +1267,6 @@ def _is_multi_user_mode() -> bool: - Multi-user BasicAuth (MCP_DEPLOYMENT_MODE=multi_user_basic) - Login Flow v2 / default OAuth (MCP_DEPLOYMENT_MODE=login_flow, or no username/password and no explicit mode) - - OAuth Token Exchange (ENABLE_TOKEN_EXCHANGE=true) Single-user mode is: - Single-user BasicAuth (username and password both set) @@ -1285,10 +1283,6 @@ def _is_multi_user_mode() -> bool: if explicit_mode == "single_user_basic": return False - # Token exchange implies OAuth multi-user - if _dynaconf.get("ENABLE_TOKEN_EXCHANGE", False): - return True - # If both username and password are set, it's single-user BasicAuth has_username = bool(_dynaconf.get("NEXTCLOUD_USERNAME")) has_password = bool(_dynaconf.get("NEXTCLOUD_PASSWORD")) diff --git a/settings.toml.example b/settings.toml.example index 84f5f6db..a3e1835f 100644 --- a/settings.toml.example +++ b/settings.toml.example @@ -48,7 +48,6 @@ enable_background_operations = false # Deprecated aliases (declared so env var overrides work) vector_sync_enabled = false enable_offline_access = false -enable_token_exchange = false # --- Token storage --- token_encryption_key = "@none" diff --git a/tests/unit/test_unified_verifier.py b/tests/unit/test_unified_verifier.py index 67b3cd0d..70ca88c5 100644 --- a/tests/unit/test_unified_verifier.py +++ b/tests/unit/test_unified_verifier.py @@ -1,8 +1,8 @@ """ Unit tests for UnifiedTokenVerifier (ADR-005). -Tests token audience validation for both multi-audience and token exchange modes -without requiring real network calls or IdP connections. +Tests multi-audience token validation without requiring real network calls or +IdP connections. """ import time @@ -35,16 +35,9 @@ def base_settings(): class TestUnifiedTokenVerifierInit: """Test UnifiedTokenVerifier initialization.""" - def test_init_multi_audience_mode(self, base_settings): - """Test verifier initialization in multi-audience mode.""" + def test_init(self, base_settings): + """Test verifier initialization (multi-audience only; no token exchange).""" verifier = UnifiedTokenVerifier(base_settings) - assert verifier.mode == "multi-audience" - assert verifier.settings == base_settings - - def test_init_always_multi_audience(self, base_settings): - """Test verifier always initializes in multi-audience mode.""" - verifier = UnifiedTokenVerifier(base_settings) - assert verifier.mode == "multi-audience" assert verifier.settings == base_settings