docs: generalize OIDC framing to support multiple IdPs

Previous round narrowed the framing too far in the other direction —
made it sound like Nextcloud OIDC is *the* IdP. The MCP server
actually supports any OIDC-compliant provider (Nextcloud's built-in
OIDC, Keycloak, AWS Cognito, Auth0, etc.) selected via
`OIDC_DISCOVERY_URL`. `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` are generic
OIDC client credentials despite the Nextcloud-flavored naming.

Code references:
- IdP discovery: app.py:607-668 (auto-detects integrated vs external
  by comparing discovered issuer to NEXTCLOUD_HOST)
- JWKS: unified_verifier.py:71-73 (dynamically discovered, not
  hard-coded to Nextcloud)
- IdP selection knob: OIDC_DISCOVERY_URL (config.py)

Changes:
- login-flow-v2.md: redraw "How It Works" diagram to show the IdP as
  a separate component; replace "Nextcloud OIDC" with "configurable
  IdP" framing throughout; add OIDC_DISCOVERY_URL to the env-var
  reference; clarify NEXTCLOUD_OIDC_CLIENT_ID/SECRET are generic OIDC
  creds; rename "OAuth Endpoints" subtitle to point at "the configured
  IdP".
- running.md: rewrite the OAuth Mode intro and Quick Start note to
  mention IdP configurability and OIDC_DISCOVERY_URL.
- configuration.md: update Best Practices "For Production" multi-user
  bullet to reference the IdP selector and generic-creds caveat.
- auth-flows.md: generalize Astrolabe-flow and Login Flow v2
  characteristics bullets — IdP and JWKS source are configurable.
- keycloak-multi-client-validation.md: REMOVE the "deprecated"
  banner I added in 35c115e. The doc covers active behavior in
  external-IdP mode (realm-level token validation by user_oidc),
  not retired direct-OAuth-to-Nextcloud architecture. Replaced with
  a scope note pointing at when this applies.

oauth-impersonation-findings.md keeps its deprecation banner — that
doc *is* about the rejected service-account / impersonation path
(ADR-002 Tier 2, "Will Not Implement"), so the deprecation framing
remains correct there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-30 02:34:38 +02:00
co-authored by Claude Opus 4.7
parent 319e82774e
commit d21be6d5e1
5 changed files with 42 additions and 26 deletions
+5 -5
View File
@@ -122,10 +122,10 @@ Astrolabe MCP Server Nextcloud OIDC
```
**Key characteristics:**
- Astrolabe has its own OAuth client registered in Nextcloud
- Tokens are validated by the MCP server using Nextcloud OIDC JWKS
- Astrolabe has its own OAuth client registered with the IdP (Nextcloud OIDC by default; Keycloak / Cognito / etc. when configured via `OIDC_DISCOVERY_URL`)
- Tokens are validated by the MCP server using the IdP's JWKS (Nextcloud OIDC's JWKS by default; whichever IdP is configured otherwise)
- Authorization check: `token.sub == requested_resource_owner`
- The same JWKS-based validation path applies under [Login Flow v2](#3-login-flow-v2) — the MCP server is an OIDC relying party of Nextcloud OIDC in both modes; Login Flow v2 only changes the MCP→Nextcloud credential leg (per-user app passwords).
- The same JWKS-based validation path applies under [Login Flow v2](#3-login-flow-v2) — the MCP server is an OIDC relying party of the configured IdP in both modes; Login Flow v2 only changes the MCP→Nextcloud credential leg (per-user app passwords).
---
@@ -158,9 +158,9 @@ 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 Nextcloud OIDC** + an OAuth facade for MCP clients (RFC 7591 DCR for MCP-client registration; the server's own RP credentials come from `NEXTCLOUD_OIDC_CLIENT_ID/SECRET`, with DCR fallback). Tokens are signed by Nextcloud OIDC and validated against Nextcloud'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-user app password obtained via Login Flow v2 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
#### First-Use Provisioning (one-time per user)
+1 -1
View File
@@ -636,7 +636,7 @@ uv run nextcloud-mcp-server --no-oauth \
Pick the mode that matches your deployment topology — there is no single "always" answer:
- **Multi-user / hosted** — use [Login Flow v2](login-flow-v2.md). The MCP server registers with Nextcloud OIDC via static `NEXTCLOUD_OIDC_CLIENT_ID` / `NEXTCLOUD_OIDC_CLIENT_SECRET` (preferred) or RFC 7591 DCR (fallback); MCP clients authenticate via OAuth 2.1 + PKCE; per-user Nextcloud access is stored as encrypted app passwords.
- **Multi-user / hosted** — use [Login Flow v2](login-flow-v2.md). The MCP server registers with the chosen IdP (Nextcloud's built-in OIDC by default; Keycloak, AWS Cognito, etc. via `OIDC_DISCOVERY_URL`) using static `NEXTCLOUD_OIDC_CLIENT_ID` / `NEXTCLOUD_OIDC_CLIENT_SECRET` (generic OIDC creds, preferred) or RFC 7591 DCR (fallback). MCP clients authenticate via OAuth 2.1 + PKCE; per-user Nextcloud access is stored as encrypted app passwords.
- **Internal multi-user** — Multi-User BasicAuth pass-through (clients send `Authorization: Basic` headers) is fully supported when users manage their own Nextcloud credentials.
- **Personal / self-hosted** — Single-User BasicAuth with a Nextcloud app password is the simplest production setup.
+1 -2
View File
@@ -1,7 +1,6 @@
# Keycloak Multi-Client Token Validation
> [!WARNING]
> **Deprecated — historical reference only.** This document describes the direct-OAuth-to-Nextcloud architecture that was retired in favor of Login Flow v2. See [ADR-022](ADR-022-deployment-mode-consolidation.md) and [Login Flow v2](login-flow-v2.md) for the current approach. Retained because [ADR-002](ADR-002-vector-sync-authentication.md) and `CLAUDE.md` still cite this investigation for context.
> **Applies to: External IdP mode (Keycloak / Cognito / etc.).** When the MCP server is configured against an external OIDC provider via `OIDC_DISCOVERY_URL`, Nextcloud's `user_oidc` app validates incoming Bearer tokens at the **realm level** — see findings below. This is independent of [Login Flow v2](login-flow-v2.md), which governs the per-user app-password leg.
## Executive Summary
+30 -13
View File
@@ -11,15 +11,24 @@ Two authentication legs, each with a different mechanism:
```
┌─────────────────┐ OAuth/OIDC ┌──────────────────┐ App password ┌─────────────────┐
│ MCP Client │ ───────────────> │ MCP Server │ ────────────────> │ Nextcloud │
│ (Claude, etc.) │ (mcp:* scopes) │ (OIDC RP of NC, │ (Basic Auth) │ (NC 16+,
│ │ │ OAuth facade, │ │ OIDC issuer)
│ (Claude, etc.) │ (mcp:* scopes) │ (OIDC RP of IdP,│ (Basic Auth) │ (NC 16+)
│ │ │ OAuth facade, │ │
│ │ │ app-pwd holder) │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ OIDC discovery + token validation
┌─────────────────┐
│ OIDC Provider │
│ (Nextcloud OIDC,│
│ Keycloak, │
│ AWS Cognito,…) │
└─────────────────┘
```
- **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 Nextcloud OIDC and exposes an OAuth facade in front of it. Tokens are signed by Nextcloud's OIDC provider, validated by the MCP server against Nextcloud's JWKS, and carry `mcp:*` scopes that gate which tools the user can call.
- **MCP server → Nextcloud (auth leg)**: The MCP server registers itself with Nextcloud's OIDC provider via static `NEXTCLOUD_OIDC_CLIENT_ID`/`SECRET` (preferred) or RFC 7591 DCR (fallback). This relationship is used for OIDC discovery, JWKS retrieval, and token validation*not* for proxying client tokens to Nextcloud APIs.
- **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.
- **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 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.
App passwords appear in **Settings → Security → Devices & Sessions** in Nextcloud and can be revoked by the user at any time.
@@ -34,16 +43,22 @@ Scope enforcement happens at the MCP server layer (defense-in-depth). See [Scope
### Required Environment Variables
```bash
# Nextcloud connection
# Nextcloud connection (data leg — always Nextcloud, regardless of which IdP authenticates clients)
NEXTCLOUD_HOST=https://your.nextcloud.example.com
# OIDC client credentials for the MCP server's relying-party relationship with Nextcloud OIDC.
# Preferred path: register a client in Nextcloud admin OIDC and set these. If both are unset
# and Nextcloud advertises a `registration_endpoint`, the server falls back to RFC 7591 DCR.
# IdP selection (auth leg). Defaults to NEXTCLOUD_HOST/.well-known/openid-configuration
# (i.e. Nextcloud's built-in OIDC). Override to point at Keycloak, AWS Cognito, etc.
# OIDC_DISCOVERY_URL=https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration
# 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.
NEXTCLOUD_OIDC_CLIENT_ID=<your-client-id>
NEXTCLOUD_OIDC_CLIENT_SECRET=<your-client-secret>
# Enable Login Flow v2 (per-user app-password provisioning for the MCP→Nextcloud data leg)
# Enable Login Flow v2 (per-user Nextcloud app-password provisioning for the data leg)
ENABLE_LOGIN_FLOW=true
# App-password storage (required for persistence across restarts)
@@ -55,6 +70,8 @@ NEXTCLOUD_MCP_SERVER_URL=https://mcp.example.com
NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.example.com # Public URL of Nextcloud
```
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.
### Generating an Encryption Key
App passwords are stored encrypted with Fernet. Generate a key once and reuse it:
@@ -196,13 +213,13 @@ Implementation: [`nextcloud_mcp_server/auth/scope_authorization.py`](../nextclou
## OAuth Endpoints
When `--oauth` is enabled, the MCP server exposes OAuth 2.1 endpoints. **These endpoints front Nextcloud OIDC** — discovery metadata, token issuance, and JWKS still come from Nextcloud; the MCP server is not a standalone OAuth issuer.
When `--oauth` is enabled, the MCP server exposes OAuth 2.1 endpoints. **These endpoints front the configured IdP** — discovery metadata, token issuance, and JWKS still come from whichever provider is selected via `OIDC_DISCOVERY_URL` (Nextcloud OIDC by default, or Keycloak / Cognito / etc.); the MCP server is not a standalone OAuth issuer.
| Endpoint | RFC | Purpose |
|----------|-----|---------|
| `GET /.well-known/oauth-authorization-server` | RFC 8414 | Server metadata (advertises Nextcloud OIDC as the upstream issuer) |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 | Server metadata (advertises the configured IdP as the upstream issuer) |
| `GET /.well-known/oauth-protected-resource/mcp` | RFC 9728 | PRM — advertises supported scopes (dynamically discovered from `@require_scopes`) |
| `POST /register` | RFC 7591 | Dynamic Client Registration (for MCP clients; see also `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` for the MCP server's own RP credentials) |
| `POST /register` | RFC 7591 | Dynamic Client Registration (for MCP clients; see also `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` for the MCP server's own RP credentials with the IdP) |
| `PUT/DELETE /register/{client_id}` | RFC 7592 | Client management with registration token |
| `GET /authorize` | RFC 6749 | Authorization endpoint (PKCE required, S256) |
| `POST /token` | RFC 6749 | Token endpoint |
+5 -5
View File
@@ -26,7 +26,7 @@ docker run -p 127.0.0.1:8000:8000 --env-file .env --rm \
ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
```
> **Note:** Under `--oauth` the MCP server is an **OIDC relying party of Nextcloud OIDC** (validates client Bearer tokens against Nextcloud's JWKS) and exposes an OAuth facade for MCP clients. It does **not** forward client OAuth tokens to Nextcloud — Nextcloud is always reached via per-user app passwords ([Login Flow v2](login-flow-v2.md)) or Basic Auth credentials.
> **Note:** Under `--oauth` the MCP server is an **OIDC relying party of a configurable IdP** (Nextcloud's built-in OIDC by default; Keycloak, AWS Cognito, etc. via `OIDC_DISCOVERY_URL`) and exposes an OAuth facade for MCP clients. Bearer tokens are validated against the IdP's JWKS. The MCP server does **not** forward client OAuth tokens to Nextcloud — Nextcloud is always reached via per-user app passwords ([Login Flow v2](login-flow-v2.md)) or Basic Auth credentials.
The server will start on `http://127.0.0.1:8000` by default.
@@ -38,12 +38,12 @@ The server will start on `http://127.0.0.1:8000` by default.
#### OAuth Mode (`--oauth`, recommended for multi-user)
The `--oauth` flag turns on the OAuth/OIDC layer. In this mode the MCP server is an **OIDC relying party of Nextcloud OIDC** (it validates Bearer tokens against Nextcloud's JWKS) and exposes an OAuth facade for MCP clients. [Login Flow v2](login-flow-v2.md) is layered on top to acquire and store per-user Nextcloud app passwords.
The `--oauth` flag turns on the OAuth/OIDC layer. In this mode the MCP server is an **OIDC relying party of a configurable IdP** — Nextcloud's built-in OIDC by default, or any OIDC-compliant provider (Keycloak, AWS Cognito, Auth0, etc.) selected via `OIDC_DISCOVERY_URL`. The MCP server validates Bearer tokens against that IdP's JWKS and exposes an OAuth facade for MCP clients. [Login Flow v2](login-flow-v2.md) is layered on top to acquire and store per-user Nextcloud app passwords for the data leg.
The MCP server registers itself with Nextcloud's OIDC provider in one of two ways:
The MCP server registers itself with the IdP in one of two ways:
- **Static client (preferred)** — set `NEXTCLOUD_OIDC_CLIENT_ID` and `NEXTCLOUD_OIDC_CLIENT_SECRET` in `.env` (matching a client you registered in Nextcloud admin → OIDC).
- **Dynamic Client Registration (fallback)** — if the static creds aren't set and Nextcloud advertises a `registration_endpoint`, the server self-registers via RFC 7591.
- **Static client (preferred)** — set `NEXTCLOUD_OIDC_CLIENT_ID` and `NEXTCLOUD_OIDC_CLIENT_SECRET` in `.env` (matching a client you registered in your IdP — Nextcloud admin → OIDC, Keycloak realm → Clients, etc.). These env-var names predate multi-IdP support; they hold generic OIDC client credentials.
- **Dynamic Client Registration (fallback)** — if the static creds aren't set and the IdP advertises a `registration_endpoint`, the server self-registers via RFC 7591.
```bash
# OAuth with static (pre-registered) client — preferred