docs: fix scope naming and round-3 reviewer feedback

The docs claimed scopes are mcp:-prefixed (mcp:notes.read,
mcp:notes.write) and that the notes.* pair "covers all Nextcloud
apps". Both are false. Per @require_scopes decorators across
nextcloud_mcp_server/server/, scopes are unprefixed and per-app:
notes.read/write, talk.read/write, files.read/write,
calendar.read/write, contacts.read/write, deck.read/write,
news.read, tables.read/write, cookbook.read/write,
todo.read/write, collectives.read/write, sharing.write,
semantic.read, plus standard OIDC scopes.

Changes:

- login-flow-v2.md: replace the false 2-row "covers all apps"
  scope table with the real per-app reference (links to
  scope_authorization.discover_all_scopes() as authoritative
  source); strip mcp: prefix from intro paragraph, sequence
  diagrams, @require_scopes example, WWW-Authenticate header
  example. Also fix sticky-session keying advice per reviewer:
  route on user identity (sub claim) rather than the raw bearer
  token, since tokens rotate on refresh.
- auth-flows.md: clarify "Astrolabe (hosted UI) → MCP" matrix
  column header; strip mcp: from sequence diagram and key
  characteristics bullet; correct "issued by MCP server" to
  "issued by configured IdP" on the Login Flow v2 token.
- authentication.md: strip mcp: from the high-level diagram and
  scope-enforcement prose; cross-link to the scope reference.
- configuration.md: add NEXTCLOUD_OIDC_CLIENT_ID,
  NEXTCLOUD_OIDC_CLIENT_SECRET, and OIDC_DISCOVERY_URL to the
  Login Flow v2 vars table — these were undocumented in the
  table after the round-2 multi-IdP fix.
- running.md: drop deprecated `version: '3.8'` from compose
  snippets (Compose v2 ignores it and emits warnings).
- testing-oidc-consent.md: fix sample authorize URL and consent
  description to use real scope names instead of mcp:-prefixed
  ones (the manual test as written would have failed with
  invalid_scope).
- CLAUDE.md: replace dead links to deleted oauth-architecture.md,
  oauth-setup.md, and audience-validation-setup.md with
  login-flow-v2.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-30 03:05:33 +02:00
co-authored by Claude Opus 4.7
parent d21be6d5e1
commit 0c6b766e7e
7 changed files with 41 additions and 30 deletions
+2 -3
View File
@@ -300,7 +300,7 @@ Use `scripts/sqlitequery.py` for all SQLite queries:
**For detailed architecture, see:** **For detailed architecture, see:**
- `docs/comparison-context-agent.md` - Overall architecture - `docs/comparison-context-agent.md` - Overall architecture
- `docs/oauth-architecture.md` - OAuth integration patterns - `docs/login-flow-v2.md` - OAuth/OIDC integration patterns and architecture
- `docs/ADR-004-progressive-consent.md` - Progressive consent implementation - `docs/ADR-004-progressive-consent.md` - Progressive consent implementation
**Core Components**: **Core Components**:
@@ -509,9 +509,8 @@ docker compose exec app php occ user_oidc:provider keycloak
**Credentials**: admin/admin (Keycloak realm: `nextcloud-mcp`) **Credentials**: admin/admin (Keycloak realm: `nextcloud-mcp`)
**For detailed Keycloak setup, see**: **For detailed Keycloak setup, see**:
- `docs/oauth-setup.md` - OAuth configuration - `docs/login-flow-v2.md` - OAuth/OIDC configuration (set `OIDC_DISCOVERY_URL` to a Keycloak realm)
- `docs/ADR-002-vector-sync-authentication.md` - Offline access architecture - `docs/ADR-002-vector-sync-authentication.md` - Offline access architecture
- `docs/audience-validation-setup.md` - Token audience validation
- `docs/keycloak-multi-client-validation.md` - Realm-level validation - `docs/keycloak-multi-client-validation.md` - Realm-level validation
## Integration Testing with Docker ## Integration Testing with Docker
+5 -5
View File
@@ -4,8 +4,8 @@ This document provides a unified reference for the auth flows in each supported
## Quick Reference Matrix ## Quick Reference Matrix
| Mode | Client → MCP → NC | Background Sync | Astrolabe → MCP | | Mode | Client → MCP → NC | Background Sync | Astrolabe (hosted UI) → MCP |
|------|-------------------|-----------------|-----------------| |------|-------------------|-----------------|------------------------------|
| [Single-User BasicAuth](#1-single-user-basicauth) | Embedded credentials | Same credentials | N/A | | [Single-User BasicAuth](#1-single-user-basicauth) | Embedded credentials | Same credentials | N/A |
| [Multi-User BasicAuth](#2-multi-user-basicauth) | Header pass-through | Stored app password (optional) | OAuth Bearer token | | [Multi-User BasicAuth](#2-multi-user-basicauth) | Header pass-through | Stored app password (optional) | OAuth Bearer token |
| [Login Flow v2](#3-login-flow-v2) | OAuth → MCP, app pwd → NC | Stored app password | OAuth Bearer token | | [Login Flow v2](#3-login-flow-v2) | OAuth → MCP, app pwd → NC | Stored app password | OAuth Bearer token |
@@ -141,8 +141,8 @@ This mode replaces the previously-supported "OAuth Single-Audience" and "OAuth T
MCP Client MCP Server Nextcloud MCP Client MCP Server Nextcloud
│ │ │ │ │ │
│── Bearer Token ────────────▶│ │ │── Bearer Token ────────────▶│ │
│ (issued by MCP server, │ │ │ (issued by configured IdP,│ │
mcp:* scopes) │ │ per-app scopes) │ │
│ │── Validate scopes ─────────│ │ │── Validate scopes ─────────│
│ │ (@require_scopes) │ │ │ (@require_scopes) │
│ │ │ │ │ │
@@ -159,7 +159,7 @@ MCP Client MCP Server Nextcloud
**Key characteristics:** **Key characteristics:**
- MCP client authenticates to MCP server via OAuth 2.1 + PKCE - MCP client authenticates to MCP server via OAuth 2.1 + PKCE
- MCP server is an **OIDC relying party of a configurable IdP** (Nextcloud OIDC by default; Keycloak, AWS Cognito, etc. via `OIDC_DISCOVERY_URL`) + an OAuth facade for MCP clients. RFC 7591 DCR is used to register the MCP-client side; the server's own RP credentials come from `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` (generic OIDC creds), with DCR fallback. Tokens are signed by the chosen IdP and validated against that IdP's JWKS. - MCP server is an **OIDC relying party of a configurable IdP** (Nextcloud OIDC by default; Keycloak, AWS Cognito, etc. via `OIDC_DISCOVERY_URL`) + an OAuth facade for MCP clients. RFC 7591 DCR is used to register the MCP-client side; the server's own RP credentials come from `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` (generic OIDC creds), with DCR fallback. Tokens are signed by the chosen IdP and validated against that IdP's JWKS.
- `mcp:*` scopes (e.g. `mcp:notes.read`, `mcp:notes.write`) gate tool access - Per-app scopes (e.g. `notes.read`, `talk.read`, `files.write`) gate tool access — see [Login Flow v2 → Scope Reference](login-flow-v2.md#scope-reference) for the full list
- Per-user app password obtained via Login Flow v2 (Nextcloud-specific protocol, used regardless of which IdP authenticated the client) and stored encrypted in SQLite - Per-user app password obtained via Login Flow v2 (Nextcloud-specific protocol, used regardless of which IdP authenticated the client) and stored encrypted in SQLite
- App passwords appear in Nextcloud's **Settings → Security → Devices & Sessions** and are user-revocable - App passwords appear in Nextcloud's **Settings → Security → Devices & Sessions** and are user-revocable
+2 -2
View File
@@ -60,10 +60,10 @@ ENABLE_MULTI_USER_BASIC_AUTH=true
The recommended mode for hosted and OAuth-based deployments. MCP clients authenticate to the MCP server via OAuth; the MCP server obtains a per-user app password from Nextcloud (via Login Flow v2) and uses HTTP Basic Auth to talk to Nextcloud APIs. The recommended mode for hosted and OAuth-based deployments. MCP clients authenticate to the MCP server via OAuth; the MCP server obtains a per-user app password from Nextcloud (via Login Flow v2) and uses HTTP Basic Auth to talk to Nextcloud APIs.
``` ```
MCP Client ──(OAuth, mcp:* scopes)──> MCP Server ──(Basic Auth, app password)──> Nextcloud MCP Client ──(OAuth, per-app scopes)──> MCP Server ──(Basic Auth, app password)──> Nextcloud
``` ```
The MCP server enforces `mcp:*` scopes at the application layer (defense-in-depth, since Nextcloud app passwords have no native scope support). The MCP server enforces per-app scopes (`notes.read`, `talk.write`, `files.read`, etc. — see [Login Flow v2 → Scope Reference](login-flow-v2.md#scope-reference)) at the application layer (defense-in-depth, since Nextcloud app passwords have no native scope support).
**See [Login Flow v2](login-flow-v2.md) for full setup, architecture, scope reference, and troubleshooting.** **See [Login Flow v2](login-flow-v2.md) for full setup, architecture, scope reference, and troubleshooting.**
+4 -1
View File
@@ -108,8 +108,11 @@ NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.instance.com
| `ENABLE_LOGIN_FLOW` | ✅ Yes | Set to `true` to enable Login Flow v2 | | `ENABLE_LOGIN_FLOW` | ✅ Yes | Set to `true` to enable Login Flow v2 |
| `TOKEN_ENCRYPTION_KEY` | ✅ Yes | Fernet key for app-password encryption — generate with `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` | | `TOKEN_ENCRYPTION_KEY` | ✅ Yes | Fernet key for app-password encryption — generate with `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` |
| `TOKEN_STORAGE_DB` | ✅ Yes | Path to SQLite DB for stored app passwords (use a persistent volume) | | `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 (OAuth issuer) | | `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_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`. |
| `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. |
See [Login Flow v2](login-flow-v2.md) for full setup, scope reference, and troubleshooting. See [Login Flow v2](login-flow-v2.md) for full setup, scope reference, and troubleshooting.
+25 -12
View File
@@ -11,7 +11,7 @@ Two authentication legs, each with a different mechanism:
``` ```
┌─────────────────┐ OAuth/OIDC ┌──────────────────┐ App password ┌─────────────────┐ ┌─────────────────┐ OAuth/OIDC ┌──────────────────┐ App password ┌─────────────────┐
│ MCP Client │ ───────────────> │ MCP Server │ ────────────────> │ Nextcloud │ │ MCP Client │ ───────────────> │ MCP Server │ ────────────────> │ Nextcloud │
│ (Claude, etc.) │ (mcp:* scopes) │ (OIDC RP of IdP,│ (Basic Auth) │ (NC 16+) │ │ (Claude, etc.) │ (per-app scopes)│ (OIDC RP of IdP,│ (Basic Auth) │ (NC 16+) │
│ │ │ OAuth facade, │ │ │ │ │ │ OAuth facade, │ │ │
│ │ │ app-pwd holder) │ │ │ │ │ │ app-pwd holder) │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘ └─────────────────┘ └──────────────────┘ └─────────────────┘
@@ -26,7 +26,7 @@ Two authentication legs, each with a different mechanism:
└─────────────────┘ └─────────────────┘
``` ```
- **MCP client → MCP server**: OAuth 2.1 with PKCE. The MCP server is **not** a standalone OAuth issuer — it acts as an OIDC relying party of a configurable identity provider and exposes an OAuth facade in front of it. The IdP is selected by `OIDC_DISCOVERY_URL` (defaults to Nextcloud's built-in OIDC); Keycloak, AWS Cognito, and other OIDC-compliant IdPs are also supported. Tokens are signed by that IdP, validated by the MCP server against the IdP's JWKS, and carry `mcp:*` scopes that gate which tools the user can call. - **MCP client → MCP server**: OAuth 2.1 with PKCE. The MCP server is **not** a standalone OAuth issuer — it acts as an OIDC relying party of a configurable identity provider and exposes an OAuth facade in front of it. The IdP is selected by `OIDC_DISCOVERY_URL` (defaults to Nextcloud's built-in OIDC); Keycloak, AWS Cognito, and other OIDC-compliant IdPs are also supported. Tokens are signed by that IdP, validated by the MCP server against the IdP's JWKS, and carry per-app scopes (`notes.read`, `talk.read`, `files.write`, …) that gate which tools the user can call.
- **MCP server → IdP (auth leg)**: The MCP server registers itself with the IdP via static `NEXTCLOUD_OIDC_CLIENT_ID`/`SECRET` (preferred — these are generic OIDC client credentials despite the Nextcloud-flavored naming, and work with any OIDC provider) or RFC 7591 DCR (fallback). This relationship is used for OIDC discovery, JWKS retrieval, and token validation. - **MCP server → IdP (auth leg)**: The MCP server registers itself with the IdP via static `NEXTCLOUD_OIDC_CLIENT_ID`/`SECRET` (preferred — these are generic OIDC client credentials despite the Nextcloud-flavored naming, and work with any OIDC provider) or RFC 7591 DCR (fallback). This relationship is used for OIDC discovery, JWKS retrieval, and token validation.
- **MCP server → Nextcloud (data leg)**: Per-user **app password** obtained via Nextcloud's native [Login Flow v2](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2). Sent as HTTP Basic Auth. Login Flow v2 is always Nextcloud's protocol regardless of which IdP authenticated the MCP client. - **MCP server → Nextcloud (data leg)**: Per-user **app password** obtained via Nextcloud's native [Login Flow v2](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2). Sent as HTTP Basic Auth. Login Flow v2 is always Nextcloud's protocol regardless of which IdP authenticated the MCP client.
@@ -123,7 +123,7 @@ Each user goes through provisioning **once**, the first time they connect. Subse
└──────┬──────┘ └────────┬─────────┘ └────────┬────────┘ └──────┬──────┘ └────────┬─────────┘ └────────┬────────┘
│ 1. OAuth PKCE │ │ │ 1. OAuth PKCE │ │
├─────────────────────────────────>│ │ ├─────────────────────────────────>│ │
│ ← access token (mcp:* scopes) │ │ │ ← access token (per-app scopes) │ │
│ │ │ │ │ │
│ 2. MCP request │ │ │ 2. MCP request │ │
├─────────────────────────────────>│ │ ├─────────────────────────────────>│ │
@@ -178,14 +178,27 @@ Nextcloud's app passwords have **no native scope support** — they grant the us
### Scope Reference ### Scope Reference
| Scope | Operations | Tool count | Scopes are **per-app** and follow an `<app>.<read|write>` pattern. There is no `mcp:` prefix.
|-------|------------|------------|
| `mcp:notes.read` | Read access (get, list, search) across all Nextcloud apps | ~36 tools |
| `mcp:notes.write` | Write access (create, update, delete) across all Nextcloud apps | ~54 tools |
> The historical `notes.read` / `notes.write` naming covers all Nextcloud apps, not just the Notes app — it's a project-wide read/write distinction. | Scope | Covers |
|-------|--------|
| `notes.read` / `notes.write` | Notes app |
| `talk.read` / `talk.write` | Talk (spreed) |
| `files.read` / `files.write` | Files / WebDAV |
| `calendar.read` / `calendar.write` | Calendar (events + tasks/VTODO) |
| `contacts.read` / `contacts.write` | Contacts (CardDAV) |
| `deck.read` / `deck.write` | Deck |
| `tables.read` / `tables.write` | Tables |
| `cookbook.read` / `cookbook.write` | Cookbook |
| `todo.read` / `todo.write` | Tasks (VTODO outside Calendar) |
| `collectives.read` / `collectives.write` | Collectives |
| `news.read` | News (read-only) |
| `sharing.write` | Share-link / share-permission management |
| `semantic.read` | Semantic search + RAG (when enabled) |
Standard OIDC scopes (`openid`, `profile`, `email`) are also supported and have no effect on tool access. The authoritative list is enumerated at runtime by [`scope_authorization.discover_all_scopes()`](../nextcloud_mcp_server/auth/scope_authorization.py) from each tool's `@require_scopes(...)` decorator and exposed via the PRM endpoint (`/.well-known/oauth-protected-resource/mcp`).
Standard OIDC scopes (`openid`, `profile`, `email`) are also accepted and have no effect on tool access.
### How Scopes Are Enforced ### How Scopes Are Enforced
@@ -193,7 +206,7 @@ Each MCP tool is decorated with `@require_scopes(...)`:
```python ```python
@mcp.tool() @mcp.tool()
@require_scopes("mcp:notes.read") @require_scopes("notes.read")
async def nc_notes_get_note(note_id: int, ctx: Context): async def nc_notes_get_note(note_id: int, ctx: Context):
... ...
``` ```
@@ -203,7 +216,7 @@ When a client calls `list_tools`, the server returns only tools the user has gra
```http ```http
HTTP/1.1 403 Forbidden HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", WWW-Authenticate: Bearer error="insufficient_scope",
scope="mcp:notes.write", scope="notes.write",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp" resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
``` ```
@@ -267,7 +280,7 @@ The user revoked it from **Settings → Security → Devices & Sessions**. Delet
The provisioning session store is in-memory; `ENABLE_LOGIN_FLOW=true` assumes a single worker. Running with `uvicorn --workers N` will cause provisioning sessions to randomly fail. For higher concurrency, scale horizontally (multiple containers behind a sticky-session load balancer) rather than within a single process. The provisioning session store is in-memory; `ENABLE_LOGIN_FLOW=true` assumes a single worker. Running with `uvicorn --workers N` will cause provisioning sessions to randomly fail. For higher concurrency, scale horizontally (multiple containers behind a sticky-session load balancer) rather than within a single process.
> **Sticky-session keying:** route on the user's OAuth bearer token (or a session cookie tied to the user) — **not** on source IP. MCP clients may not maintain stable IPs between the request that initiates provisioning and the polling request that completes it, so IP-based affinity will silently break the flow. A stable per-user identifier from the `Authorization` header is the right key. > **Sticky-session keying:** route on the **user identity** (e.g. the `sub` claim from the OAuth Bearer token) — **not** the raw token value, and **not** source IP. Bearer tokens rotate on refresh, which would silently break token-value affinity if a refresh lands between the request that initiates provisioning and the polling request that completes it. MCP clients may also not maintain stable IPs across those requests. A stable per-user identifier extracted from the `Authorization` header (e.g. `sub`) is the right key.
## See Also ## See Also
-4
View File
@@ -93,8 +93,6 @@ docker run -p 127.0.0.1:8000:8000 --env-file .env \
Create `docker-compose.yml`: Create `docker-compose.yml`:
```yaml ```yaml
version: '3.8'
services: services:
mcp: mcp:
image: ghcr.io/cbcoutinho/nextcloud-mcp-server:latest image: ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
@@ -369,8 +367,6 @@ docker-compose logs -f mcp
For production deployments, use Docker Compose with the recommended settings: For production deployments, use Docker Compose with the recommended settings:
```yaml ```yaml
version: '3.8'
services: services:
mcp: mcp:
image: ghcr.io/cbcoutinho/nextcloud-mcp-server:latest image: ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
+3 -3
View File
@@ -190,7 +190,7 @@ docker compose logs -f mcp-oauth
1. Get client_id from the JWT client JSON 1. Get client_id from the JWT client JSON
2. Visit in browser: 2. Visit in browser:
``` ```
http://localhost:8080/apps/oidc/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost:8001/oauth/callback&scope=openid+profile+email+mcp:notes.read+mcp:notes.write&state=test123 http://localhost:8080/apps/oidc/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost:8001/oauth/callback&scope=openid+profile+email+notes.read+notes.write&state=test123
``` ```
### 3. Expected Behavior ### 3. Expected Behavior
@@ -203,8 +203,8 @@ http://localhost:8080/apps/oidc/authorize?client_id=YOUR_CLIENT_ID&response_type
- ✓ Basic authentication (openid) - required, cannot deselect - ✓ Basic authentication (openid) - required, cannot deselect
- ✓ Profile information (profile) - ✓ Profile information (profile)
- ✓ Email address (email) - ✓ Email address (email)
-mcp:notes.read (custom scope, shown as-is) - ✓ notes.read (custom scope, shown as-is)
-mcp:notes.write (custom scope, shown as-is) - ✓ notes.write (custom scope, shown as-is)
- "Allow" and "Deny" buttons - "Allow" and "Deny" buttons
3. User selects scopes and clicks "Allow" 3. User selects scopes and clicks "Allow"
4. Authorization proceeds with selected scopes 4. Authorization proceeds with selected scopes