Merge pull request #908 from cbcoutinho/docs/login-flow-static-oidc-client-907
docs(login-flow): require static OIDC client; remove dead token-exchange mode
This commit is contained in:
+12
-8
@@ -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())"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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 ✅
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+22
-3
@@ -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=<fernet-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=<client-id-from-nextcloud>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<client-secret-from-nextcloud>
|
||||
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
+57
-5
@@ -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=<your-client-id>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<your-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=<fernet-key> # see "Generating an encryption key" below
|
||||
TOKEN_ENCRYPTION_KEY=<your-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://<your-mcp-server>/oauth/callback`
|
||||
- **Flow / response type:** authorization **code**
|
||||
- **Type:** **confidential** (so it issues a client secret)
|
||||
- **Resource identifier:** `https://<your-mcp-server>/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=<your-client-id>
|
||||
# - NEXTCLOUD_OIDC_CLIENT_SECRET=<your-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=<your-fernet-key>
|
||||
- TOKEN_ENCRYPTION_KEY=<your-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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -207,24 +207,7 @@ php occ webhook_listeners:remove <webhook-id>
|
||||
|
||||
---
|
||||
|
||||
### 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=<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
|
||||
|
||||
-34
@@ -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)
|
||||
# ============================================
|
||||
|
||||
@@ -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
|
||||
+32
-19
@@ -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=<your-client-id>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<your-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=<your-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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user