diff --git a/docker-compose.yml b/docker-compose.yml index 795c6ccb..762d7b71 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 @@ -293,6 +295,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/configuration-migration-v2.md b/docs/configuration-migration-v2.md index 3aac3740..b20f8cb3 100644 --- a/docs/configuration-migration-v2.md +++ b/docs/configuration-migration-v2.md @@ -500,8 +500,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..f7957f1a 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= @@ -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) + - **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 @@ -329,6 +349,31 @@ 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. + +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/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..4368f307 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,14 +18,17 @@ 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) NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000 @@ -68,10 +73,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