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:
co-authored by
Claude Opus 4.7
parent
d21be6d5e1
commit
0c6b766e7e
@@ -300,7 +300,7 @@ Use `scripts/sqlitequery.py` for all SQLite queries:
|
||||
|
||||
**For detailed architecture, see:**
|
||||
- `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
|
||||
|
||||
**Core Components**:
|
||||
@@ -509,9 +509,8 @@ docker compose exec app php occ user_oidc:provider keycloak
|
||||
**Credentials**: admin/admin (Keycloak realm: `nextcloud-mcp`)
|
||||
|
||||
**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/audience-validation-setup.md` - Token audience validation
|
||||
- `docs/keycloak-multi-client-validation.md` - Realm-level validation
|
||||
|
||||
## Integration Testing with Docker
|
||||
|
||||
+5
-5
@@ -4,8 +4,8 @@ This document provides a unified reference for the auth flows in each supported
|
||||
|
||||
## 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 |
|
||||
| [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 |
|
||||
@@ -141,8 +141,8 @@ This mode replaces the previously-supported "OAuth Single-Audience" and "OAuth T
|
||||
MCP Client MCP Server Nextcloud
|
||||
│ │ │
|
||||
│── Bearer Token ────────────▶│ │
|
||||
│ (issued by MCP server, │ │
|
||||
│ mcp:* scopes) │ │
|
||||
│ (issued by configured IdP,│ │
|
||||
│ per-app scopes) │ │
|
||||
│ │── Validate scopes ─────────│
|
||||
│ │ (@require_scopes) │
|
||||
│ │ │
|
||||
@@ -159,7 +159,7 @@ MCP Client MCP Server Nextcloud
|
||||
**Key characteristics:**
|
||||
- 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:*` 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
|
||||
- App passwords appear in Nextcloud's **Settings → Security → Devices & Sessions** and are user-revocable
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
```
|
||||
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.**
|
||||
|
||||
|
||||
@@ -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 |
|
||||
| `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) |
|
||||
| `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_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.
|
||||
|
||||
|
||||
+25
-12
@@ -11,7 +11,7 @@ Two authentication legs, each with a different mechanism:
|
||||
```
|
||||
┌─────────────────┐ OAuth/OIDC ┌──────────────────┐ App password ┌─────────────────┐
|
||||
│ 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, │ │ │
|
||||
│ │ │ 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 → 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 │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ ← access token (mcp:* scopes) │ │
|
||||
│ ← access token (per-app scopes) │ │
|
||||
│ │ │
|
||||
│ 2. MCP request │ │
|
||||
├─────────────────────────────────>│ │
|
||||
@@ -178,14 +178,27 @@ Nextcloud's app passwords have **no native scope support** — they grant the us
|
||||
|
||||
### Scope Reference
|
||||
|
||||
| Scope | Operations | Tool count |
|
||||
|-------|------------|------------|
|
||||
| `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 |
|
||||
Scopes are **per-app** and follow an `<app>.<read|write>` pattern. There is no `mcp:` prefix.
|
||||
|
||||
> 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
|
||||
|
||||
@@ -193,7 +206,7 @@ Each MCP tool is decorated with `@require_scopes(...)`:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
@require_scopes("mcp:notes.read")
|
||||
@require_scopes("notes.read")
|
||||
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/1.1 403 Forbidden
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
> **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
|
||||
|
||||
|
||||
@@ -93,8 +93,6 @@ docker run -p 127.0.0.1:8000:8000 --env-file .env \
|
||||
Create `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcp:
|
||||
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:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcp:
|
||||
image: ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
|
||||
|
||||
@@ -190,7 +190,7 @@ docker compose logs -f mcp-oauth
|
||||
1. Get client_id from the JWT client JSON
|
||||
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
|
||||
@@ -203,8 +203,8 @@ http://localhost:8080/apps/oidc/authorize?client_id=YOUR_CLIENT_ID&response_type
|
||||
- ✓ Basic authentication (openid) - required, cannot deselect
|
||||
- ✓ Profile information (profile)
|
||||
- ✓ Email address (email)
|
||||
- ✓ mcp:notes.read (custom scope, shown as-is)
|
||||
- ✓ mcp:notes.write (custom scope, shown as-is)
|
||||
- ✓ notes.read (custom scope, shown as-is)
|
||||
- ✓ notes.write (custom scope, shown as-is)
|
||||
- "Allow" and "Deny" buttons
|
||||
3. User selects scopes and clicks "Allow"
|
||||
4. Authorization proceeds with selected scopes
|
||||
|
||||
Reference in New Issue
Block a user