docs: pivot to Login Flow v2; add Astrolabe Cloud hosted offering
Replace the seven OAuth-to-Nextcloud docs (oauth-setup, quickstart-oauth, oauth-architecture, oauth-upstream-status, oauth-troubleshooting, jwt-oauth-reference, audience-validation-setup) with a single new docs/login-flow-v2.md. The deprecated flow required upstream user_oidc patches that were never merged; Login Flow v2 is the forward-looking multi-user mode (see ADR-022), and works with stock Nextcloud 16+. Rewrite docs/authentication.md and docs/auth-flows.md around three modes: Single-User BasicAuth, Multi-User BasicAuth pass-through, and Login Flow v2. Update README to add an Astrolabe Cloud (https://astrolabecloud.com) callout for users who prefer not to self-host, drop the OAuth deployment mode from the auth table, simplify the Docker block, and trim the Examples and Security sections. Sweep configuration.md, installation.md, troubleshooting.md, running.md, and semantic-search-architecture.md to replace links to the deleted docs and update deprecated mode names. 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
19c4911954
commit
1306849353
@@ -15,6 +15,9 @@ This is a **dedicated standalone MCP server** designed for external MCP clients
|
||||
> [!NOTE]
|
||||
> **Looking for AI features inside Nextcloud?** Nextcloud also provides [Context Agent](https://github.com/nextcloud/context_agent), which powers the Assistant app and runs as an ExApp inside Nextcloud. See [docs/comparison-context-agent.md](docs/comparison-context-agent.md) for a detailed comparison of use cases.
|
||||
|
||||
> [!TIP]
|
||||
> **Don't want to self-host?** [Astrolabe Cloud](https://astrolabecloud.com) is a managed hosting service for this MCP server, aimed at users and teams who want advanced features like background sync and semantic search without operating the infrastructure themselves. The service is currently under development — sign up on the landing page to join the early-adopter list.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run the server locally with [uvx](https://docs.astral.sh/uv/) (no installation required):
|
||||
@@ -52,36 +55,16 @@ Or add it directly to your MCP client configuration (e.g. `claude_desktop_config
|
||||
For full features including semantic search, run with Docker:
|
||||
|
||||
```bash
|
||||
# 1. Create a minimal configuration
|
||||
cat > .env << EOF
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
NEXTCLOUD_USERNAME=your_username
|
||||
NEXTCLOUD_PASSWORD=your_app_password
|
||||
EOF
|
||||
|
||||
# 2. Start the server
|
||||
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm \
|
||||
docker run -p 127.0.0.1:8000:8000 --rm \
|
||||
-e NEXTCLOUD_HOST=https://your.nextcloud.instance.com \
|
||||
-e NEXTCLOUD_USERNAME=your_username \
|
||||
-e NEXTCLOUD_PASSWORD=your_app_password \
|
||||
ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
|
||||
|
||||
# 3. Test the connection
|
||||
curl http://127.0.0.1:8000/health/ready
|
||||
|
||||
# 4. Connect to the endpoint
|
||||
http://127.0.0.1:8000/mcp
|
||||
```
|
||||
|
||||
**Docker Compose Profiles** (for development/testing):
|
||||
Then connect your MCP client (Claude Desktop, IDEs, `mcp dev`, etc.) to `http://127.0.0.1:8000/mcp`.
|
||||
|
||||
```bash
|
||||
docker compose --profile single-user up -d # Port 8000
|
||||
docker compose --profile multi-user-basic up -d # Port 8003
|
||||
docker compose --profile oauth up -d # Port 8001
|
||||
docker compose --profile login-flow up -d # Port 8004
|
||||
```
|
||||
|
||||
**Next Steps:**
|
||||
- Connect your MCP client (Claude Desktop, IDEs, `mcp dev`, etc.)
|
||||
- See [docs/installation.md](docs/installation.md) for other deployment options. For Kubernetes (Helm), see [cbcoutinho/helm-charts](https://github.com/cbcoutinho/helm-charts)
|
||||
For Kubernetes, see [cbcoutinho/helm-charts](https://github.com/cbcoutinho/helm-charts). For other deployment options and Compose profiles, see [docs/installation.md](docs/installation.md).
|
||||
|
||||
## Key Features
|
||||
|
||||
@@ -90,7 +73,7 @@ docker compose --profile login-flow up -d # Port 8004
|
||||
- **Semantic Search (Experimental)** - Optional vector-powered search for Notes, Files, News items, and Deck cards (requires Qdrant + Ollama)
|
||||
- **Document Processing** - OCR and text extraction from PDFs, DOCX, images with progress notifications
|
||||
- **Flexible Deployment** - Docker, Kubernetes ([Helm chart](https://github.com/cbcoutinho/helm-charts)), VM, or local installation
|
||||
- **Production-Ready Auth** - Basic Auth with app passwords (recommended) or OAuth2/OIDC (experimental)
|
||||
- **Production-Ready Auth** - Basic Auth with app passwords; multi-user via Login Flow v2 (OAuth-to-MCP supported, with app-password conversion to Nextcloud)
|
||||
- **Multiple Transports** - streamable-http (default) and stdio
|
||||
|
||||
## Supported Apps
|
||||
@@ -113,121 +96,35 @@ Want to see another Nextcloud app supported? [Open an issue](https://github.com/
|
||||
|
||||
## Authentication
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **OAuth2/OIDC is experimental** and requires a manual patch to the `user_oidc` app:
|
||||
> - **Required patch**: Bearer token support ([issue #1221](https://github.com/nextcloud/user_oidc/issues/1221))
|
||||
> - **Impact**: Without the patch, most app-specific APIs fail with 401 errors
|
||||
> - **Recommendation**: Use Basic Auth for production until upstream patches are merged
|
||||
>
|
||||
> See [docs/oauth-upstream-status.md](docs/oauth-upstream-status.md) for patch status and workarounds.
|
||||
The MCP server authenticates to Nextcloud using **app-specific passwords** (Basic Auth). Three deployment modes are supported:
|
||||
|
||||
**Recommended:** Basic Auth with app-specific passwords provides secure, production-ready authentication. See [docs/authentication.md](docs/authentication.md) for setup details and OAuth configuration.
|
||||
| Mode | Best for |
|
||||
|------|----------|
|
||||
| Single-User (BasicAuth) | Personal use, development, single-user deployments |
|
||||
| Multi-User (BasicAuth pass-through) | Multi-user setups where clients send credentials via Authorization header |
|
||||
| Multi-User (Login Flow v2) | Multi-user / hosted deployments — clients authenticate to the MCP server via OAuth, and the server obtains a per-user app password from Nextcloud and uses it transparently |
|
||||
|
||||
### Authentication Modes
|
||||
OAuth-direct-to-Nextcloud is no longer supported (it required upstream patches to `user_oidc` that were never merged). Login Flow v2 replaces it for multi-user deployments and works with stock Nextcloud.
|
||||
|
||||
The server supports four authentication modes:
|
||||
|
||||
**Single-User (BasicAuth):**
|
||||
- One set of credentials shared by all MCP clients
|
||||
- Simple setup: username + app password in environment variables
|
||||
- All clients access Nextcloud as the same user
|
||||
- Best for: Personal use, development, single-user deployments
|
||||
|
||||
**Multi-User (BasicAuth Pass-Through):**
|
||||
- MCP clients send credentials via Authorization header
|
||||
- Server passes through to Nextcloud (stateless by default)
|
||||
- Optional offline access for background operations (`ENABLE_MULTI_USER_BASIC_AUTH=true`)
|
||||
- Best for: Multi-user setups without OAuth infrastructure
|
||||
|
||||
**Multi-User (OAuth):**
|
||||
- Each MCP client authenticates separately with their own Nextcloud account
|
||||
- Per-user scopes and permissions (clients only see tools they're authorized for)
|
||||
- More secure: tokens expire, credentials never shared with server
|
||||
- Best for: Teams, multi-user deployments, production environments with multiple users
|
||||
- Requires: Patches to the `user_oidc` app (experimental)
|
||||
|
||||
**Multi-User (Login Flow v2):**
|
||||
- Uses Nextcloud's native Login Flow v2 to obtain per-user app passwords
|
||||
- No OAuth patches required — works with stock Nextcloud
|
||||
- Each user authenticates via browser, server manages app passwords
|
||||
- Best for: Multi-user deployments without OAuth infrastructure (`ENABLE_LOGIN_FLOW=true`)
|
||||
- Experimental: See [ADR-022](docs/ADR-022-deployment-mode-consolidation.md) for details
|
||||
|
||||
See [docs/authentication.md](docs/authentication.md) for detailed setup instructions.
|
||||
See [docs/authentication.md](docs/authentication.md) for setup instructions.
|
||||
|
||||
## Semantic Search
|
||||
|
||||
The server provides an experimental RAG pipeline to enable _Semantic Search_ that enables MCP clients to find information in Nextcloud based on **meaning** rather than just keywords. Instead of matching "machine learning" only when those exact words appear, it understands that "neural networks," "AI models," and "deep learning" are semantically related concepts.
|
||||
An experimental RAG pipeline that lets MCP clients find Nextcloud content by **meaning** rather than keywords — a query for "car" also surfaces notes about "vehicle" or "transportation". Disabled by default (`ENABLE_SEMANTIC_SEARCH=false`); requires a vector database and embedding service. See [docs/semantic-search-architecture.md](docs/semantic-search-architecture.md) and [docs/configuration.md](docs/configuration.md).
|
||||
|
||||
**Example:**
|
||||
- **Keyword search**: Query "car" only finds notes containing "car"
|
||||
- **Semantic search**: Query "car" also finds notes about "automobile," "vehicle," "sedan," "transportation"
|
||||
|
||||
This enables natural language queries and helps discover related content across your Nextcloud notes.
|
||||
|
||||
> [!NOTE]
|
||||
> **Semantic Search is experimental and opt-in:**
|
||||
> - Disabled by default (`ENABLE_SEMANTIC_SEARCH=false`)
|
||||
> - Currently supports Notes app only (multi-app support planned)
|
||||
> - Requires additional infrastructure: vector database + embedding service
|
||||
> - Answer generation (`nc_semantic_search_answer`) requires MCP client sampling support
|
||||
>
|
||||
> See [docs/semantic-search-architecture.md](docs/semantic-search-architecture.md) for architecture details and [docs/configuration.md](docs/configuration.md) for setup instructions.
|
||||
> [!TIP]
|
||||
> **Don't want to run Qdrant and an embedding service?** [Astrolabe Cloud](https://astrolabecloud.com) (under development) provides semantic search and background sync as a managed service.
|
||||
|
||||
## Documentation
|
||||
|
||||
### Getting Started
|
||||
- **[Installation](docs/installation.md)** - Docker, local, or VM deployment. [Helm chart](https://github.com/cbcoutinho/helm-charts) for Kubernetes
|
||||
- **[Configuration](docs/configuration.md)** - Environment variables and advanced options
|
||||
- **[Authentication](docs/authentication.md)** - Basic Auth vs OAuth2/OIDC setup
|
||||
- **[Running the Server](docs/running.md)** - Start, manage, and troubleshoot
|
||||
|
||||
### Features
|
||||
- **[App Documentation](docs/)** - Notes, Calendar, Contacts, WebDAV, Deck, Cookbook, Tables
|
||||
- **[Document Processing](docs/configuration.md#document-processing)** - OCR and text extraction setup
|
||||
- **[Semantic Search Architecture](docs/semantic-search-architecture.md)** - Experimental vector search (Notes, Files, News items, Deck cards; opt-in)
|
||||
- **[Vector Sync UI Guide](docs/user-guide/vector-sync-ui.md)** - Browser interface for semantic search visualization and testing
|
||||
|
||||
### Advanced Topics
|
||||
- **[OAuth Architecture](docs/oauth-architecture.md)** - How OAuth works (experimental)
|
||||
- **[OAuth Quick Start](docs/quickstart-oauth.md)** - 5-minute OAuth setup
|
||||
- **[OAuth Setup Guide](docs/oauth-setup.md)** - Detailed OAuth configuration
|
||||
- **[Troubleshooting](docs/troubleshooting.md)** - Common issues and solutions
|
||||
- **[Comparison with Context Agent](docs/comparison-context-agent.md)** - When to use each approach
|
||||
|
||||
## Examples
|
||||
|
||||
### Create a Note
|
||||
```
|
||||
AI: "Create a note called 'Meeting Notes' with today's agenda"
|
||||
→ Uses nc_notes_create_note tool
|
||||
```
|
||||
|
||||
### Import Recipes
|
||||
```
|
||||
AI: "Import the recipe from https://www.example.com/recipe/chocolate-cake"
|
||||
→ Uses nc_cookbook_import_recipe tool with schema.org metadata extraction
|
||||
```
|
||||
|
||||
### Schedule Meetings
|
||||
```
|
||||
AI: "Schedule a team meeting for next Tuesday at 2pm"
|
||||
→ Uses nc_calendar_create_event tool
|
||||
```
|
||||
|
||||
### Manage Files
|
||||
```
|
||||
AI: "Create a folder called 'Project X' and move all PDFs there"
|
||||
→ Uses nc_webdav_create_directory and nc_webdav_move tools
|
||||
```
|
||||
|
||||
### Semantic Search (Experimental, Opt-in)
|
||||
```
|
||||
AI: "Find notes related to machine learning concepts"
|
||||
→ Uses nc_semantic_search to find semantically similar notes (requires Qdrant + Ollama setup)
|
||||
```
|
||||
|
||||
**Note:** For AI-generated answers with citations, use `nc_semantic_search_answer` (requires MCP client with sampling support).
|
||||
- **[Installation](docs/installation.md)** — Docker, Compose profiles, local, VM
|
||||
- **[Configuration](docs/configuration.md)** — Environment variables, document processing, semantic search setup
|
||||
- **[Authentication](docs/authentication.md)** — Basic Auth, Login Flow v2
|
||||
- **[Running the Server](docs/running.md)** — Start, manage, troubleshoot
|
||||
- **[App Documentation](docs/)** — Per-app guides (Notes, Calendar, Contacts, WebDAV, Deck, Cookbook, Tables)
|
||||
- **[Semantic Search Architecture](docs/semantic-search-architecture.md)** + **[Vector Sync UI](docs/user-guide/vector-sync-ui.md)**
|
||||
- **[Login Flow v2](docs/login-flow-v2.md)** — recommended multi-user setup (architecture, env vars, scope reference, troubleshooting)
|
||||
- **[Troubleshooting](docs/troubleshooting.md)** · **[Comparison with Context Agent](docs/comparison-context-agent.md)**
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -241,13 +138,6 @@ Contributions are welcome!
|
||||
|
||||
[](https://mseep.ai/app/cbcoutinho-nextcloud-mcp-server)
|
||||
|
||||
This project takes security seriously:
|
||||
- Production-ready Basic Auth with app-specific passwords
|
||||
- OAuth2/OIDC support (experimental, requires upstream patches)
|
||||
- Per-user access tokens
|
||||
- No credential storage in OAuth mode
|
||||
- Regular security assessments
|
||||
|
||||
Found a security issue? Please report it privately to the maintainers.
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,521 +0,0 @@
|
||||
# Audience Validation Setup
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains the **separate clients architecture** for Keycloak → MCP Server → Nextcloud integration, following OAuth 2.0 best practices and RFC 8707 (Resource Indicators).
|
||||
|
||||
## Architecture: Separate Clients Pattern
|
||||
|
||||
```
|
||||
Keycloak Realm: nextcloud-mcp
|
||||
├── Client: "nextcloud" (Resource Server)
|
||||
│ └── Represents Nextcloud as a protected resource
|
||||
│ └── Used by user_oidc for bearer token validation
|
||||
│ └── Validates tokens with aud="nextcloud"
|
||||
│
|
||||
└── Client: "nextcloud-mcp-server" (OAuth Client)
|
||||
└── MCP Server uses this to REQUEST tokens
|
||||
└── Issues tokens with aud="nextcloud" (targeting resource)
|
||||
└── Future: aud=["nextcloud", "other-service"]
|
||||
|
||||
Token Flow:
|
||||
MCP Server (client: nextcloud-mcp-server)
|
||||
↓ requests token from Keycloak
|
||||
Token issued:
|
||||
- aud: "nextcloud" (intended for Nextcloud resource)
|
||||
- azp: "nextcloud-mcp-server" (requested by MCP Server)
|
||||
- preferred_username: "admin" (on behalf of user)
|
||||
↓ sent to Nextcloud API
|
||||
Nextcloud user_oidc (client: nextcloud)
|
||||
✓ validates aud matches configured client_id
|
||||
```
|
||||
|
||||
**Key Benefits**:
|
||||
- ✅ **Proper OAuth separation**: OAuth client ≠ resource server
|
||||
- ✅ **Future extensibility**: MCP Server can request multi-resource tokens
|
||||
- ✅ **RFC 8707 compliance**: Audience indicates intended resource
|
||||
- ✅ **Clear requester identification**: azp claim identifies MCP Server
|
||||
|
||||
## Token Claims
|
||||
|
||||
Tokens issued by the `nextcloud-mcp-server` client contain:
|
||||
|
||||
- **`aud: "nextcloud"`** - Audience: Token intended for Nextcloud resource server (matches user_oidc client_id)
|
||||
- **`azp: "nextcloud-mcp-server"`** - Authorized Party: Identifies MCP Server as the OAuth client that requested the token
|
||||
- **`preferred_username: "admin"`** - User identifier (Keycloak uses this for password grant; `sub` for authorization_code grant)
|
||||
- **`scope: "openid profile email offline_access"`** - Requested scopes including offline access for background jobs
|
||||
|
||||
**How user_oidc Validates**:
|
||||
1. SelfEncodedValidator checks: `aud == user_oidc.client_id`?
|
||||
- ✓ "nextcloud" == "nextcloud" → PASS
|
||||
2. Fast JWT verification with JWKS (no HTTP call to userinfo endpoint)
|
||||
3. User provisioned based on `preferred_username` or `sub` claim
|
||||
|
||||
**For Background Jobs**:
|
||||
- MCP Server stores encrypted refresh tokens
|
||||
- Refreshes access tokens when needed
|
||||
- All tokens have `aud: "nextcloud"` → validated by user_oidc
|
||||
- No admin credentials required
|
||||
|
||||
## Configuration
|
||||
|
||||
The configuration requires **two separate clients** in Keycloak:
|
||||
|
||||
1. **`nextcloud`** - Resource server client (for user_oidc validation)
|
||||
2. **`nextcloud-mcp-server`** - OAuth client (for MCP Server to request tokens)
|
||||
|
||||
### 1. Keycloak - Create Resource Server Client
|
||||
|
||||
First, create the `nextcloud` client that represents Nextcloud as a resource server:
|
||||
|
||||
**Via Keycloak Admin API:**
|
||||
|
||||
```bash
|
||||
# Get admin token
|
||||
ADMIN_TOKEN=$(curl -X POST "http://localhost:8888/realms/master/protocol/openid-connect/token" \
|
||||
-d "grant_type=password" \
|
||||
-d "client_id=admin-cli" \
|
||||
-d "username=admin" \
|
||||
-d "password=admin" | jq -r '.access_token')
|
||||
|
||||
# Create 'nextcloud' resource server client
|
||||
curl -X POST "http://localhost:8888/admin/realms/nextcloud-mcp/clients" \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "nextcloud",
|
||||
"name": "Nextcloud Resource Server",
|
||||
"description": "Resource server for Nextcloud APIs - used by user_oidc for bearer token validation",
|
||||
"enabled": true,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "nextcloud-secret-change-in-production",
|
||||
"bearerOnly": true,
|
||||
"standardFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"serviceAccountsEnabled": false,
|
||||
"publicClient": false
|
||||
}'
|
||||
```
|
||||
|
||||
**Via Realm Export** (`keycloak/realm-export.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "nextcloud",
|
||||
"name": "Nextcloud Resource Server",
|
||||
"enabled": true,
|
||||
"bearerOnly": true,
|
||||
"secret": "nextcloud-secret-change-in-production"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Keycloak - Create OAuth Client with Audience Mapper
|
||||
|
||||
Next, create the `nextcloud-mcp-server` client that MCP Server uses to request tokens:
|
||||
|
||||
**Via Keycloak Admin API:**
|
||||
|
||||
```bash
|
||||
# Create 'nextcloud-mcp-server' OAuth client
|
||||
curl -X POST "http://localhost:8888/admin/realms/nextcloud-mcp/clients" \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "nextcloud-mcp-server",
|
||||
"name": "Nextcloud MCP Server",
|
||||
"enabled": true,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "mcp-secret-change-in-production",
|
||||
"standardFlowEnabled": true,
|
||||
"directAccessGrantsEnabled": true,
|
||||
"redirectUris": ["http://localhost:*/callback"]
|
||||
}'
|
||||
|
||||
# Get client internal ID
|
||||
CLIENT_ID=$(curl "http://localhost:8888/admin/realms/nextcloud-mcp/clients" \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" | jq -r '.[] | select(.clientId=="nextcloud-mcp-server") | .id')
|
||||
|
||||
# Add audience mapper targeting 'nextcloud' resource
|
||||
curl -X POST "http://localhost:8888/admin/realms/nextcloud-mcp/clients/$CLIENT_ID/protocol-mappers/models" \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "audience-nextcloud",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-audience-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"included.custom.audience": "nextcloud",
|
||||
"access.token.claim": "true",
|
||||
"id.token.claim": "false"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Option B: Via Realm Export** (for infrastructure-as-code)
|
||||
|
||||
Update `keycloak/realm-export.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "nextcloud-mcp-server",
|
||||
"name": "Nextcloud MCP Server",
|
||||
"protocolMappers": [
|
||||
{
|
||||
"name": "audience-nextcloud-mcp-server",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-audience-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"included.custom.audience": "nextcloud-mcp-server",
|
||||
"access.token.claim": "true",
|
||||
"id.token.claim": "false"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Then re-import realm or restart Keycloak.
|
||||
|
||||
**Option C: Via Keycloak Admin UI**
|
||||
|
||||
1. Go to Keycloak Admin Console → Realm → Clients → `nextcloud-mcp-server`
|
||||
2. Click "Client scopes" tab
|
||||
3. Click "Add client scope" → "Create dedicated scope"
|
||||
4. Add protocol mapper: "Audience"
|
||||
- Mapper Type: `Audience`
|
||||
- Included Custom Audience: `nextcloud`
|
||||
- Add to access token: ON
|
||||
- Add to ID token: OFF
|
||||
|
||||
### 3. Nextcloud user_oidc - Configure Resource Server Client
|
||||
|
||||
Configure user_oidc to use the `nextcloud` resource server client:
|
||||
|
||||
```bash
|
||||
docker compose exec app php occ user_oidc:provider keycloak \
|
||||
--clientid="nextcloud" \
|
||||
--clientsecret="nextcloud-secret-change-in-production" \
|
||||
--discoveryuri="http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration" \
|
||||
--check-bearer=1 \
|
||||
--bearer-provisioning=1 \
|
||||
--unique-uid=1 \
|
||||
--mapping-uid="sub" \
|
||||
--mapping-display-name="name" \
|
||||
--mapping-email="email"
|
||||
```
|
||||
|
||||
**Result**: user_oidc validates tokens with `aud="nextcloud"` using SelfEncodedValidator (fast JWT verification).
|
||||
|
||||
### 3. Nextcloud user_oidc - Realm-Level Validation
|
||||
|
||||
Nextcloud's `user_oidc` app validates at **realm level** via userinfo endpoint:
|
||||
|
||||
- ✅ **No configuration needed** - works automatically
|
||||
- ✅ Validates any token from Keycloak realm
|
||||
- ✅ Audience check is **optional** (disabled by default)
|
||||
|
||||
**Optional: Disable strict audience checking** (if enabled):
|
||||
|
||||
```bash
|
||||
docker compose exec app php occ config:app:set user_oidc \
|
||||
selfencoded_bearer_validation_audience_check --value=false --type=boolean
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
### 1. Check Token Claims
|
||||
|
||||
```bash
|
||||
# Get token from Keycloak
|
||||
TOKEN=$(curl -X POST "http://localhost:8888/realms/nextcloud-mcp/protocol/openid-connect/token" \
|
||||
-d "grant_type=password" \
|
||||
-d "client_id=nextcloud-mcp-server" \
|
||||
-d "client_secret=mcp-secret-change-in-production" \
|
||||
-d "username=admin" \
|
||||
-d "password=admin" | jq -r '.access_token')
|
||||
|
||||
# Decode JWT
|
||||
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq '.'
|
||||
|
||||
# Should show:
|
||||
{
|
||||
"aud": "nextcloud", # ✓ Intended for Nextcloud
|
||||
"azp": "nextcloud-mcp-server", # ✓ Requested by MCP Server
|
||||
"iss": "http://localhost:8888/realms/nextcloud-mcp",
|
||||
"scope": "openid email profile offline_access",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Test with Nextcloud API
|
||||
|
||||
```bash
|
||||
# Token should be accepted
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"http://localhost:8080/ocs/v2.php/cloud/capabilities"
|
||||
|
||||
# Should return HTTP 200 OK
|
||||
```
|
||||
|
||||
### 3. Test Audience Rejection
|
||||
|
||||
```bash
|
||||
# Get token from different client (without audience mappers)
|
||||
TOKEN_WRONG=$(curl -X POST "http://localhost:8888/realms/nextcloud-mcp/protocol/openid-connect/token" \
|
||||
-d "grant_type=password" \
|
||||
-d "client_id=test-client-b" \
|
||||
-d "client_secret=test-secret-b" \
|
||||
-d "username=admin" \
|
||||
-d "password=admin" | jq -r '.access_token')
|
||||
|
||||
# This token has NO audience claim - should be rejected by MCP server
|
||||
# (But accepted by Nextcloud user_oidc which validates at realm level)
|
||||
```
|
||||
|
||||
## Token Flow Example
|
||||
|
||||
### Successful Request (Background Job)
|
||||
|
||||
```
|
||||
1. User authorizes MCP Client via OAuth
|
||||
└─ MCP Server gets refresh token (stored encrypted)
|
||||
|
||||
2. Background worker needs to sync data
|
||||
└─ MCP Server refreshes access token from Keycloak
|
||||
└─ Token issued with aud: "nextcloud", azp: "nextcloud-mcp-server"
|
||||
|
||||
3. MCP Server → Nextcloud API (with token)
|
||||
└─ user_oidc validates via userinfo endpoint ✓
|
||||
└─ Nextcloud identifies:
|
||||
- Token intended for Nextcloud (aud: "nextcloud")
|
||||
- Request from MCP Server (azp: "nextcloud-mcp-server")
|
||||
- On behalf of user (sub: "user-id")
|
||||
|
||||
4. Success! MCP Server can act on behalf of user in background.
|
||||
```
|
||||
|
||||
### Rejected Request
|
||||
|
||||
```
|
||||
1. Attacker gets token for different client
|
||||
└─ Token has aud: "other-service"
|
||||
|
||||
2. Attacker → Nextcloud API (with wrong token)
|
||||
└─ user_oidc validates via userinfo endpoint
|
||||
└─ Token validation fails (invalid/expired/wrong realm)
|
||||
└─ HTTP 401 Unauthorized
|
||||
|
||||
3. Request blocked - token not valid for this realm/service
|
||||
```
|
||||
|
||||
## OAuth Flows and User Consent
|
||||
|
||||
### When Does the User Grant Consent?
|
||||
|
||||
User consent happens during the **Authorization Code Flow** (production OAuth):
|
||||
|
||||
```
|
||||
1. User clicks "Connect" in MCP Client (e.g., Claude Desktop)
|
||||
2. MCP Client initiates OAuth flow by opening browser to Keycloak:
|
||||
https://keycloak/realms/nextcloud-mcp/protocol/openid-connect/auth?
|
||||
client_id=nextcloud-mcp-server&
|
||||
redirect_uri=<mcp-client-redirect-uri>&
|
||||
response_type=code&
|
||||
scope=openid profile email offline_access
|
||||
|
||||
3. Keycloak shows login screen (if not logged in)
|
||||
4. **Keycloak shows consent screen:**
|
||||
"Nextcloud MCP Server wants to access your Nextcloud data on your behalf"
|
||||
Requested permissions:
|
||||
- Access your profile (openid, profile, email)
|
||||
- Offline access (background operations with refresh tokens)
|
||||
|
||||
5. User clicks "Allow" → grants consent
|
||||
6. Keycloak redirects back to MCP Client with authorization code
|
||||
7. MCP Client exchanges code for tokens (receives access + refresh tokens)
|
||||
8. MCP Client shares tokens with MCP Server via MCP protocol
|
||||
9. MCP Server stores refresh token encrypted for background operations
|
||||
```
|
||||
|
||||
**Key Architecture Notes:**
|
||||
- **MCP Server is a protected resource** (requires OAuth to access)
|
||||
- **MCP Client** (Claude Desktop) is the OAuth client that initiates the flow
|
||||
- **MCP Client handles the redirect** and token exchange with Keycloak
|
||||
- **MCP Client shares refresh token** with MCP Server so it can act on behalf of user in background
|
||||
|
||||
**Key Points:**
|
||||
- ✅ **Explicit user consent** before any access
|
||||
- ✅ **Scopes displayed** so user knows what's being requested
|
||||
- ✅ **Offline access** must be explicitly granted (for background jobs)
|
||||
- ✅ **Revocable** - user can revoke consent in Keycloak at any time
|
||||
|
||||
### Grant Types
|
||||
|
||||
Our architecture supports multiple OAuth grant types:
|
||||
|
||||
**1. Authorization Code + PKCE (Production)**
|
||||
```
|
||||
Use case: Interactive login from MCP clients
|
||||
Consent: Yes - explicit user authorization
|
||||
Tokens: Access token + Refresh token (if offline_access granted)
|
||||
Security: PKCE prevents authorization code interception
|
||||
```
|
||||
|
||||
**2. Password Grant (Testing Only)**
|
||||
```
|
||||
Use case: Integration testing with docker-compose
|
||||
Consent: No - username/password provided directly
|
||||
Tokens: Access token + Refresh token
|
||||
Security: NOT for production - exposes user credentials
|
||||
```
|
||||
|
||||
**3. Refresh Token Grant (Background Jobs)**
|
||||
```
|
||||
Use case: MCP Server refreshing expired access tokens
|
||||
Consent: No new consent - uses previously granted refresh token
|
||||
Tokens: New access token (refresh token may rotate)
|
||||
Security: Refresh tokens stored encrypted, rotated on use
|
||||
```
|
||||
|
||||
## Authentication Strategies for Background Jobs
|
||||
|
||||
> **Note on Service Account Tokens**: Service account tokens (`client_credentials` grant) were evaluated but **rejected** as they create Nextcloud user accounts (e.g., `service-account-{client_id}`) which violates OAuth "act on-behalf-of" principles. See ADR-002 "Will Not Implement" section for details.
|
||||
|
||||
### Current Approach: Offline Access with Refresh Tokens
|
||||
|
||||
The MCP server uses **offline_access** scope to enable background operations:
|
||||
|
||||
**How it works:**
|
||||
1. User grants `offline_access` scope during OAuth consent
|
||||
2. MCP Client receives refresh token from Keycloak
|
||||
3. MCP Client shares refresh token with MCP Server via MCP protocol
|
||||
4. MCP Server stores refresh token encrypted (see ADR-002)
|
||||
5. Background jobs exchange refresh token for fresh access tokens as needed
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Works today with Keycloak and all OIDC providers
|
||||
- ✅ Standard OAuth pattern (RFC 6749)
|
||||
- ✅ Explicit user consent to `offline_access` scope
|
||||
- ✅ MCP Server can act on behalf of user in background
|
||||
|
||||
**Limitations:**
|
||||
- ⚠️ Requires secure token storage on MCP Server
|
||||
- ⚠️ MCP Client must trust MCP Server with refresh token
|
||||
- ⚠️ Weak audit trail - API requests appear to come from user directly
|
||||
- ⚠️ No visibility that MCP Server is the actual actor
|
||||
|
||||
### Token Exchange with Delegation (ADR-002 Tier 2 - Implemented)
|
||||
|
||||
**RFC 8693 Delegation** would provide better audit trail and security:
|
||||
|
||||
**How it would work:**
|
||||
1. User grants `may_act:nextcloud-mcp-server` scope during authentication
|
||||
2. Subject token includes: `{ "may_act": { "client": "nextcloud-mcp-server" } }`
|
||||
3. MCP Server has its own service account token (actor_token)
|
||||
4. Background job requests token exchange:
|
||||
- `subject_token` (user's token with may_act claim)
|
||||
- `actor_token` (mcp-server's service token)
|
||||
5. Keycloak validates actor matches may_act claim
|
||||
6. Returns delegated token: `{ "sub": "user", "act": "nextcloud-mcp-server" }`
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Better audit trail - Nextcloud APIs see both user and actor
|
||||
- ✅ No token storage needed (tokens generated on-demand)
|
||||
- ✅ Fine-grained permissions via `may_act` claim
|
||||
- ✅ User explicitly consents to MCP Server acting on their behalf
|
||||
- ✅ RFC 8693 compliant
|
||||
|
||||
**Current Status:**
|
||||
- ❌ **NOT implemented in Keycloak yet** ([Issue #38279](https://github.com/keycloak/keycloak/issues/38279))
|
||||
- ❌ Would require custom implementation or waiting for upstream
|
||||
- 📝 Proposal includes `act` claim and `may_act` consent mechanism
|
||||
|
||||
**Why Not Available:**
|
||||
- Keycloak supports **impersonation** (changes `sub` claim), but not **delegation** (`act` claim)
|
||||
- Impersonation has poor audit trail (actor invisible)
|
||||
- Delegation proposal is open but not implemented yet
|
||||
|
||||
**Reference:** See `docs/ADR-002-vector-sync-authentication.md` for detailed comparison of authentication tiers.
|
||||
|
||||
## Security Benefits
|
||||
|
||||
1. **Intent Validation**: Tokens explicitly declare Nextcloud as the intended recipient via `aud` claim
|
||||
2. **Requester Identification**: The `azp` claim identifies MCP Server as the requester
|
||||
3. **User Context**: The `sub` claim preserves user identity for audit and authorization
|
||||
4. **Background Jobs**: Refresh tokens enable MCP Server to act on behalf of users without admin credentials
|
||||
5. **OAuth Standards**: Follows RFC 8707 (Resource Indicators) and RFC 6749 (OAuth 2.0)
|
||||
|
||||
**Current Limitations:**
|
||||
- API requests from background jobs appear to come from user directly (no `act` claim yet)
|
||||
- See "Authentication Strategies for Background Jobs" section for future delegation support
|
||||
|
||||
## Token Claims
|
||||
|
||||
### Key Claims
|
||||
|
||||
- **`aud: "nextcloud"`** - Audience: Token intended for Nextcloud APIs
|
||||
- **`azp: "nextcloud-mcp-server"`** - Authorized Party: MCP Server requested the token
|
||||
- **`sub: "user-id"`** - Subject: User on whose behalf the request is made
|
||||
- **`scope: "openid profile email offline_access"`** - Requested scopes including offline access for background jobs
|
||||
|
||||
### Client Naming
|
||||
|
||||
The Keycloak client is named `nextcloud-mcp-server` to clarify:
|
||||
- **MCP Server** uses this client to get tokens for Nextcloud
|
||||
- **MCP Clients** (like Claude Desktop) connect to MCP Server via separate OAuth flows
|
||||
- **Not** named "mcp-client" to avoid confusion about which component is the client
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Token Has No Audience
|
||||
|
||||
**Symptom**: `"aud": null` in decoded JWT
|
||||
|
||||
**Cause**: Protocol mappers not configured
|
||||
|
||||
**Solution**: Add audience mappers via Keycloak Admin API (see Configuration section)
|
||||
|
||||
### MCP Server Rejects Token
|
||||
|
||||
**Symptom**: HTTP 401 with "JWT validation failed"
|
||||
|
||||
**Cause**: Token audience doesn't match expected value
|
||||
|
||||
**Solution**:
|
||||
1. Check token has correct `aud` claim
|
||||
2. Verify MCP server expects correct audience value in code
|
||||
3. Check logs for specific JWT validation error
|
||||
|
||||
### Nextcloud Rejects Token
|
||||
|
||||
**Symptom**: HTTP 401 from Nextcloud API
|
||||
|
||||
**Cause**: User not provisioned or token invalid
|
||||
|
||||
**Solution**:
|
||||
1. Check user_oidc provider is configured: `php occ user_oidc:provider keycloak`
|
||||
2. Check bearer validation enabled: `--check-bearer=1`
|
||||
3. Test token with userinfo endpoint: `curl -H "Authorization: Bearer $TOKEN" http://keycloak/realms/.../userinfo`
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **Multi-client validation**: `docs/keycloak-multi-client-validation.md`
|
||||
- **ADR-002**: `docs/ADR-002-vector-sync-authentication.md`
|
||||
- **OAuth setup**: `docs/oauth-setup.md`
|
||||
- **Keycloak integration**: `docs/keycloak-integration.md` (if created)
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 8707 - Resource Indicators for OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc8707)
|
||||
- [OIDC Core - ID Token aud claim](https://openid.net/specs/openid-connect-core-1_0.html#IDToken)
|
||||
- [Keycloak Audience Protocol Mappers](https://www.keycloak.org/docs/latest/server_admin/#_audience)
|
||||
+87
-196
@@ -1,23 +1,22 @@
|
||||
# Authentication Flows by Deployment Mode
|
||||
|
||||
This document provides a unified reference for authentication flows across all deployment modes. For configuration details, see [Authentication](authentication.md). For OAuth protocol details, see [OAuth Architecture](oauth-architecture.md).
|
||||
This document provides a unified reference for the auth flows in each supported deployment mode. For configuration details, see [Authentication](authentication.md). For Login Flow v2 architecture and setup, see [Login Flow v2](login-flow-v2.md).
|
||||
|
||||
## Quick Reference Matrix
|
||||
|
||||
| Mode | Client → MCP → NC | Background Sync | Astrolabe → MCP |
|
||||
|------|-------------------|-----------------|-----------------|
|
||||
| [Single-User BasicAuth](#1-single-user-basicauth) | Embedded credentials | Same credentials | N/A |
|
||||
| [Multi-User BasicAuth](#2-multi-user-basicauth) | Header pass-through | App password (optional) | Bearer token |
|
||||
| [OAuth Single-Audience](#3-oauth-single-audience-default) | Multi-audience token | Refresh token exchange | Bearer token |
|
||||
| [OAuth Token Exchange](#4-oauth-token-exchange-rfc-8693) | RFC 8693 exchange | Refresh token exchange | 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 |
|
||||
|
||||
## Communication Patterns
|
||||
|
||||
This document covers three distinct communication patterns:
|
||||
|
||||
1. **MCP Client → MCP Server → Nextcloud**: Interactive tool calls initiated by users through MCP clients (Claude Desktop, etc.)
|
||||
2. **MCP Server → Nextcloud**: Background operations like vector sync that run without user interaction
|
||||
3. **Astrolabe → MCP Server**: Nextcloud app backend communication for settings UI and unified search
|
||||
1. **MCP Client → MCP Server → Nextcloud**: Interactive tool calls initiated by users through MCP clients (Claude Desktop, claude.ai, custom clients).
|
||||
2. **MCP Server → Nextcloud**: Background operations like vector sync that run without user interaction.
|
||||
3. **Astrolabe → MCP Server**: Astrolabe app backend communication for settings UI and unified search.
|
||||
|
||||
---
|
||||
|
||||
@@ -47,23 +46,21 @@ MCP Client MCP Server Nextcloud
|
||||
- No MCP-level authentication required (server trusts local clients)
|
||||
- All requests use the same Nextcloud user
|
||||
|
||||
**Implementation:** `context.py:78-79` - Returns shared client from lifespan context
|
||||
**Implementation:** `context.py` — returns the shared client from lifespan context
|
||||
|
||||
#### Background Sync
|
||||
|
||||
Uses the same embedded credentials as interactive requests. The background job accesses Nextcloud with the configured username/password.
|
||||
|
||||
**Implementation:** Background jobs use `get_settings()` to access credentials
|
||||
|
||||
#### Astrolabe Integration
|
||||
|
||||
Not applicable - Astrolabe is only used in multi-user deployments where users need personal settings and token management.
|
||||
Not applicable — Astrolabe is only used in multi-user deployments where users need personal settings and per-user state.
|
||||
|
||||
---
|
||||
|
||||
### 2. Multi-User BasicAuth
|
||||
|
||||
**Use Case:** Internal deployment where users provide their own credentials via HTTP headers.
|
||||
**Use Case:** Internal deployment where users provide their own Nextcloud credentials via HTTP headers.
|
||||
|
||||
#### MCP Client → MCP Server → Nextcloud
|
||||
|
||||
@@ -83,38 +80,31 @@ MCP Client MCP Server Nextcloud
|
||||
```
|
||||
|
||||
**Key characteristics:**
|
||||
- `BasicAuthMiddleware` extracts credentials from `Authorization: Basic` header
|
||||
- `BasicAuthMiddleware` extracts credentials from the `Authorization: Basic` header
|
||||
- Credentials passed through to Nextcloud (not stored)
|
||||
- Client created per-request from extracted credentials
|
||||
- Stateless - no credential storage between requests
|
||||
|
||||
**Implementation:** `context.py:187-248` - `_get_client_from_basic_auth()` extracts credentials from request state
|
||||
- Stateless — no credential storage between requests
|
||||
|
||||
#### Background Sync (Optional)
|
||||
|
||||
Requires `ENABLE_OFFLINE_ACCESS=true`. Users can store app passwords via Astrolabe for background operations.
|
||||
If users provision an app password (via Astrolabe or `nc_auth_provision_access`), the server can run background jobs on their behalf:
|
||||
|
||||
```
|
||||
Astrolabe MCP Server Nextcloud
|
||||
│ │ │
|
||||
│── Store App Password ──────▶│ │
|
||||
│── Store app password ──────▶│ │
|
||||
│ (via management API) │ │
|
||||
│ │── Store in SQLite ────────▶│
|
||||
│ │ (encrypted) │
|
||||
│ │── Encrypt + persist ──────▶│
|
||||
│ │ (SQLite, Fernet) │
|
||||
│◀── Confirmation ────────────│ │
|
||||
│ │ │
|
||||
│ [Background Job] │ │
|
||||
│ [Background job] │ │
|
||||
│ │── Retrieve app password ──▶│
|
||||
│ │ (from encrypted storage) │
|
||||
│ │── HTTP + BasicAuth ───────▶│
|
||||
│ │ (stored app password) │
|
||||
│ │◀── API Response ───────────│
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- `ENABLE_OFFLINE_ACCESS=true`
|
||||
- `TOKEN_ENCRYPTION_KEY` for credential encryption
|
||||
- `TOKEN_STORAGE_DB` for SQLite storage path
|
||||
**Requirements:** `TOKEN_ENCRYPTION_KEY`, `TOKEN_STORAGE_DB`.
|
||||
|
||||
#### Astrolabe → MCP Server
|
||||
|
||||
@@ -132,173 +122,89 @@ Astrolabe MCP Server Nextcloud OIDC
|
||||
```
|
||||
|
||||
**Key characteristics:**
|
||||
- Astrolabe has its own OAuth client (`astrolabe_client_id` in Nextcloud config)
|
||||
- Tokens are validated by MCP server using Nextcloud OIDC JWKS
|
||||
- Astrolabe has its own OAuth client registered in Nextcloud
|
||||
- Tokens are validated by the MCP server using Nextcloud OIDC JWKS
|
||||
- Authorization check: `token.sub == requested_resource_owner`
|
||||
- Any valid Nextcloud OIDC token accepted (relaxed audience validation per ADR-018)
|
||||
|
||||
**Implementation:** `unified_verifier.py:120-183` - `verify_token_for_management_api()` validates without strict audience check
|
||||
|
||||
---
|
||||
|
||||
### 3. OAuth Single-Audience (Default)
|
||||
### 3. Login Flow v2
|
||||
|
||||
**Use Case:** Multi-user deployment with OAuth authentication. Tokens work for both MCP and Nextcloud.
|
||||
**Use Case:** Hosted multi-user deployments, OAuth-based MCP clients (claude.ai, Astrolabe Cloud), production. Recommended for any setup where MCP clients shouldn't handle Nextcloud credentials directly.
|
||||
|
||||
This is the default mode when `NEXTCLOUD_USERNAME`/`NEXTCLOUD_PASSWORD` are not set.
|
||||
This mode replaces the previously-supported "OAuth Single-Audience" and "OAuth Token Exchange" modes, both of which required upstream Nextcloud patches that were never merged. See [ADR-022](ADR-022-deployment-mode-consolidation.md) for the rationale.
|
||||
|
||||
#### MCP Client → MCP Server → Nextcloud
|
||||
#### MCP Client → MCP Server → Nextcloud (steady state)
|
||||
|
||||
```
|
||||
MCP Client MCP Server Nextcloud
|
||||
│ │ │
|
||||
│── Bearer Token ────────────▶│ │
|
||||
│ aud: ["mcp-server", │ │
|
||||
│ "nextcloud"] │ │
|
||||
│ │── Validate MCP audience ──▶│
|
||||
│ │ (UnifiedTokenVerifier) │
|
||||
│ (issued by MCP server, │ │
|
||||
│ mcp:* scopes) │ │
|
||||
│ │── Validate scopes ─────────│
|
||||
│ │ (@require_scopes) │
|
||||
│ │ │
|
||||
│ │── HTTP + Same Token ──────▶│
|
||||
│ │ Authorization: Bearer │
|
||||
│ │ (multi-audience token) │
|
||||
│ │── Lookup user's │
|
||||
│ │ stored app password │
|
||||
│ │ │
|
||||
│ │ NC validates its own aud │
|
||||
│ │── HTTP + BasicAuth ───────▶│
|
||||
│ │ Authorization: Basic │
|
||||
│ │ (per-user app password) │
|
||||
│ │◀── API Response ───────────│
|
||||
│◀── Tool Result ─────────────│ │
|
||||
```
|
||||
|
||||
**Key characteristics:**
|
||||
- Token contains both audiences: `aud: ["mcp-server", "nextcloud"]`
|
||||
- MCP server validates only MCP audience (per RFC 7519)
|
||||
- Nextcloud independently validates its own audience
|
||||
- No token exchange needed - same token used throughout
|
||||
- Stateless operation for interactive requests
|
||||
- MCP client authenticates to MCP server via OAuth 2.1 + PKCE
|
||||
- MCP server is the OAuth authorization server (DCR via RFC 7591)
|
||||
- `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
|
||||
- App passwords appear in Nextcloud's **Settings → Security → Devices & Sessions** and are user-revocable
|
||||
|
||||
**Token validation flow:**
|
||||
1. `UnifiedTokenVerifier.verify_token()` validates MCP audience
|
||||
2. Token passed directly to Nextcloud via `get_client_from_context()`
|
||||
3. Nextcloud validates its own audience when receiving API calls
|
||||
#### First-Use Provisioning (one-time per user)
|
||||
|
||||
**Implementation:**
|
||||
- `unified_verifier.py:185-252` - `_verify_mcp_audience()` validates MCP audience only
|
||||
- `context.py:96-99` - Uses token directly in multi-audience mode
|
||||
```
|
||||
MCP Client MCP Server Nextcloud
|
||||
│ │ │
|
||||
│── Bearer Token + request ──▶│ │
|
||||
│ │ No stored app password │
|
||||
│ │ │
|
||||
│◀── Elicit URL or 401 ───────│ │
|
||||
│ "Visit <login-url>" │ │
|
||||
│ │── POST /index.php/login/v2▶│
|
||||
│ │◀── login_url, poll_token ──│
|
||||
│ │ │
|
||||
│ User opens login_url in browser, authenticates, "Grant"│
|
||||
│ ──────────────────────────────────────────────────────▶│
|
||||
│ │ │
|
||||
│ │── Poll endpoint (bg) ─────▶│
|
||||
│ │◀── loginName, appPassword ─│
|
||||
│ │ │
|
||||
│ │── Encrypt + store │
|
||||
│ │ in tokens.db │
|
||||
│ │ │
|
||||
│── Retry request ───────────▶│── Basic Auth as above ────▶│
|
||||
```
|
||||
|
||||
#### Background Sync
|
||||
|
||||
Requires `ENABLE_OFFLINE_ACCESS=true`. Uses stored refresh tokens to obtain access tokens for background operations.
|
||||
Uses the same per-user app password retrieved from encrypted storage. No token refresh needed — Nextcloud app passwords don't expire (until the user revokes them).
|
||||
|
||||
```
|
||||
MCP Server Nextcloud OIDC
|
||||
MCP Server Nextcloud
|
||||
│ │
|
||||
[Background Job starts] │ │
|
||||
│── Get refresh token ──────▶│
|
||||
│ (from encrypted storage) │
|
||||
[Background job starts] │ │
|
||||
│── Retrieve app password ──▶│
|
||||
│ (per user, from SQLite) │
|
||||
│ │
|
||||
│── Token refresh request ──▶│
|
||||
│ grant_type=refresh_token │
|
||||
│ scope=openid profile ... │
|
||||
│◀── New access + refresh ───│
|
||||
│ (rotation) │
|
||||
│ │
|
||||
│── Store rotated refresh ──▶│
|
||||
│ (encrypted) │
|
||||
│ │
|
||||
│── HTTP + Access Token ────▶│
|
||||
│ Authorization: Bearer │
|
||||
│── HTTP + BasicAuth ───────▶│
|
||||
│◀── API Response ───────────│
|
||||
```
|
||||
|
||||
**Key characteristics:**
|
||||
- Refresh tokens stored encrypted in SQLite (`TOKEN_STORAGE_DB`)
|
||||
- Nextcloud OIDC rotates refresh tokens on every use (one-time use)
|
||||
- `TokenBrokerService` handles token lifecycle
|
||||
- Per-user locking prevents race conditions during concurrent refresh
|
||||
|
||||
**Implementation:**
|
||||
- `token_broker.py:269-362` - `get_background_token()` handles refresh with locking
|
||||
- `token_broker.py:428-509` - `_refresh_access_token_with_scopes()` exchanges refresh token
|
||||
|
||||
#### Astrolabe → MCP Server
|
||||
|
||||
Same as Multi-User BasicAuth. See [Astrolabe → MCP Server](#astrolabe--mcp-server) above.
|
||||
|
||||
---
|
||||
|
||||
### 4. OAuth Token Exchange (RFC 8693)
|
||||
|
||||
**Use Case:** Multi-user deployment where MCP tokens are separate from Nextcloud tokens. Provides stronger security boundaries.
|
||||
|
||||
Enabled by `ENABLE_TOKEN_EXCHANGE=true`.
|
||||
|
||||
#### MCP Client → MCP Server → Nextcloud
|
||||
|
||||
```
|
||||
MCP Client MCP Server Nextcloud OIDC
|
||||
│ │ │
|
||||
│── Bearer Token ────────────▶│ │
|
||||
│ aud: "mcp-server" │ │
|
||||
│ (MCP audience only) │ │
|
||||
│ │── Validate MCP audience ──▶│
|
||||
│ │ │
|
||||
│ │── RFC 8693 Exchange ──────▶│
|
||||
│ │ grant_type= │
|
||||
│ │ urn:ietf:params:oauth: │
|
||||
│ │ grant-type:token-exchange
|
||||
│ │ subject_token=<mcp-token>│
|
||||
│ │ requested_audience= │
|
||||
│ │ "nextcloud" │
|
||||
│ │◀── Delegated Token ────────│
|
||||
│ │ aud: "nextcloud" │
|
||||
│ │ │
|
||||
│ │── HTTP + Delegated Token ─▶│
|
||||
│ │ Authorization: Bearer │
|
||||
│ │◀── API Response ───────────│
|
||||
│◀── Tool Result ─────────────│ │
|
||||
```
|
||||
|
||||
**Key characteristics:**
|
||||
- Strict audience separation: MCP token has `aud: "mcp-server"` only
|
||||
- Server exchanges for Nextcloud-audience token on each request
|
||||
- Ephemeral delegated tokens (not cached by default)
|
||||
- Strongest security boundary between MCP and Nextcloud access
|
||||
|
||||
**Token exchange details:**
|
||||
- Uses RFC 8693 "urn:ietf:params:oauth:grant-type:token-exchange"
|
||||
- Subject token: MCP access token
|
||||
- Requested audience: Nextcloud resource URI
|
||||
- Result: Short-lived token scoped for Nextcloud
|
||||
|
||||
**Implementation:**
|
||||
- `token_broker.py:220-267` - `get_session_token()` performs on-demand exchange
|
||||
- `token_exchange.py` - `exchange_token_for_delegation()` implements RFC 8693
|
||||
- `context.py:88-94` - Routes to session client in exchange mode
|
||||
|
||||
#### Background Sync
|
||||
|
||||
Same as OAuth Single-Audience. Uses stored refresh tokens from Flow 2 provisioning.
|
||||
|
||||
```
|
||||
MCP Server Nextcloud OIDC
|
||||
│ │
|
||||
[User provisions access] │ │
|
||||
│── Flow 2 OAuth ───────────▶│
|
||||
│ client_id="mcp-server" │
|
||||
│ scope=offline_access ... │
|
||||
│◀── Refresh Token ──────────│
|
||||
│ (stored encrypted) │
|
||||
│ │
|
||||
[Background Job runs later] │ │
|
||||
│── Refresh for background ─▶│
|
||||
│ (same as single-audience)│
|
||||
```
|
||||
|
||||
**Key difference from interactive:**
|
||||
- Interactive: On-demand token exchange per request
|
||||
- Background: Uses pre-provisioned refresh tokens (Flow 2)
|
||||
|
||||
#### Astrolabe → MCP Server
|
||||
|
||||
Same as Multi-User BasicAuth. See [Astrolabe → MCP Server](#astrolabe--mcp-server) above.
|
||||
Same as Multi-User BasicAuth — see [Astrolabe → MCP Server](#astrolabe--mcp-server) above.
|
||||
|
||||
---
|
||||
|
||||
@@ -306,57 +212,42 @@ Same as Multi-User BasicAuth. See [Astrolabe → MCP Server](#astrolabe--mcp-ser
|
||||
|
||||
### Single-User BasicAuth
|
||||
```bash
|
||||
NEXTCLOUD_HOST=http://localhost:8080
|
||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||
NEXTCLOUD_USERNAME=admin
|
||||
NEXTCLOUD_PASSWORD=password
|
||||
NEXTCLOUD_PASSWORD=<app-password>
|
||||
```
|
||||
|
||||
### Multi-User BasicAuth
|
||||
```bash
|
||||
NEXTCLOUD_HOST=http://nextcloud.example.com
|
||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
||||
|
||||
# Optional: For background sync
|
||||
ENABLE_OFFLINE_ACCESS=true
|
||||
TOKEN_ENCRYPTION_KEY=<32-byte-key>
|
||||
TOKEN_STORAGE_DB=/data/tokens.db
|
||||
# Optional: app-password storage for background sync
|
||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||
TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
```
|
||||
|
||||
### OAuth Single-Audience (Default)
|
||||
### Login Flow v2
|
||||
```bash
|
||||
NEXTCLOUD_HOST=http://nextcloud.example.com
|
||||
# No username/password triggers OAuth mode
|
||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||
ENABLE_LOGIN_FLOW=true
|
||||
|
||||
# Optional: Static client credentials (instead of DCR)
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=<client-id>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<client-secret>
|
||||
# Required for app-password storage
|
||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||
TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
|
||||
# Optional: For background sync
|
||||
ENABLE_OFFLINE_ACCESS=true
|
||||
TOKEN_ENCRYPTION_KEY=<32-byte-key>
|
||||
TOKEN_STORAGE_DB=/data/tokens.db
|
||||
# Public URLs (for browser redirects)
|
||||
NEXTCLOUD_MCP_SERVER_URL=https://mcp.example.com
|
||||
NEXTCLOUD_PUBLIC_ISSUER_URL=https://nextcloud.example.com
|
||||
```
|
||||
|
||||
### OAuth Token Exchange
|
||||
```bash
|
||||
NEXTCLOUD_HOST=http://nextcloud.example.com
|
||||
ENABLE_TOKEN_EXCHANGE=true
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=<client-id>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<client-secret>
|
||||
|
||||
# Optional: For background sync
|
||||
ENABLE_OFFLINE_ACCESS=true
|
||||
TOKEN_ENCRYPTION_KEY=<32-byte-key>
|
||||
TOKEN_STORAGE_DB=/data/tokens.db
|
||||
```
|
||||
See [Login Flow v2](login-flow-v2.md) for full setup, scope reference, and troubleshooting.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Authentication](authentication.md) - Configuration details and setup guides
|
||||
- [OAuth Architecture](oauth-architecture.md) - Deep OAuth protocol details
|
||||
- [ADR-004: Progressive Consent](ADR-004-mcp-application-oauth.md) - Dual OAuth flow architecture
|
||||
- [ADR-005: Token Audience Validation](ADR-005-token-audience-validation.md) - Audience validation strategy
|
||||
- [ADR-018: Nextcloud PHP App](ADR-018-nextcloud-php-app-for-settings-ui.md) - Astrolabe integration
|
||||
- [ADR-020: Deployment Modes](ADR-020-deployment-modes-and-configuration-validation.md) - Mode detection and validation
|
||||
- [Authentication](authentication.md) — mode comparison and selection
|
||||
- [Login Flow v2](login-flow-v2.md) — multi-user setup details
|
||||
- [Configuration](configuration.md) — environment variable reference
|
||||
- [ADR-022](ADR-022-deployment-mode-consolidation.md) — design rationale for mode consolidation
|
||||
|
||||
+65
-220
@@ -1,252 +1,97 @@
|
||||
# Authentication
|
||||
|
||||
The Nextcloud MCP server supports two authentication modes for connecting to your Nextcloud instance.
|
||||
The Nextcloud MCP server authenticates to Nextcloud using **app-specific passwords** (HTTP Basic Auth). It supports three deployment modes that differ in how those credentials are sourced.
|
||||
|
||||
## Authentication Modes Comparison
|
||||
## Mode Comparison
|
||||
|
||||
| Mode | Status | Security | Use Case |
|
||||
|------|--------|----------|----------|
|
||||
| **OAuth2/OIDC** | ✅ Recommended | 🔒 High | Production deployments, multi-user scenarios |
|
||||
| **Basic Auth** | ⚠️ Legacy | ⚠️ Lower | Development, backward compatibility |
|
||||
| Mode | How credentials are obtained | Best for |
|
||||
|------|------------------------------|----------|
|
||||
| **Single-User (BasicAuth)** | App password in environment variables | Personal use, development, single-tenant deployments |
|
||||
| **Multi-User (BasicAuth pass-through)** | MCP client sends credentials in HTTP `Authorization` header | Internal multi-user setups where users manage their own Nextcloud credentials |
|
||||
| **Multi-User (Login Flow v2)** | Per-user app password obtained via Nextcloud's [Login Flow v2](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2), stored encrypted | Hosted deployments, OAuth-based MCP clients (claude.ai, Astrolabe Cloud), production multi-user |
|
||||
|
||||
## OAuth2/OIDC (Recommended)
|
||||
> **OAuth-direct-to-Nextcloud is no longer supported.** It required upstream patches to `user_oidc` that were never merged. Login Flow v2 replaces it for multi-user deployments and works with stock Nextcloud 16+. See [ADR-022](ADR-022-deployment-mode-consolidation.md) for the rationale.
|
||||
|
||||
OAuth2/OIDC authentication provides secure, token-based authentication following modern security standards.
|
||||
## Single-User (BasicAuth)
|
||||
|
||||
### Architecture
|
||||
|
||||
The Nextcloud MCP Server acts as an **OAuth 2.0 Resource Server**, protecting access to Nextcloud resources:
|
||||
|
||||
```
|
||||
MCP Client ←→ MCP Server (Resource Server) ←→ Nextcloud (Authorization Server + APIs)
|
||||
OAuth Flow with PKCE Bearer Token Auth
|
||||
```
|
||||
|
||||
**Key Components**:
|
||||
- **MCP Server**: OAuth Resource Server (validates tokens, provides MCP tools)
|
||||
- **Nextcloud `oidc` app**: OAuth Authorization Server (issues tokens)
|
||||
- **Nextcloud `user_oidc` app**: Token validation middleware
|
||||
- **MCP Client**: Any MCP-compatible client (Claude, custom clients)
|
||||
|
||||
For detailed architecture, see [OAuth Architecture](oauth-architecture.md).
|
||||
|
||||
### Required Nextcloud Apps
|
||||
|
||||
OAuth authentication requires **two Nextcloud apps** to work together:
|
||||
|
||||
#### 1. `oidc` - OIDC Identity Provider
|
||||
**Purpose:** Makes Nextcloud an OAuth2/OIDC authorization server
|
||||
|
||||
**Provides:**
|
||||
- OAuth2 authorization endpoint (`/apps/oidc/authorize`)
|
||||
- Token endpoint (`/apps/oidc/token`)
|
||||
- User info endpoint (`/apps/oidc/userinfo`)
|
||||
- JWKS endpoint for token validation (`/apps/oidc/jwks`)
|
||||
- Dynamic client registration endpoint (`/apps/oidc/register`)
|
||||
|
||||
**Installation:** Available in Nextcloud App Store under "Security"
|
||||
|
||||
#### 2. `user_oidc` - OpenID Connect User Backend
|
||||
**Purpose:** Authenticates users and validates Bearer tokens
|
||||
|
||||
**Provides:**
|
||||
- Bearer token validation against the OIDC provider
|
||||
- User authentication via OIDC
|
||||
- Session management for authenticated users
|
||||
|
||||
**Installation:** Available in Nextcloud App Store under "Security"
|
||||
|
||||
**Important:** The `user_oidc` app requires a patch for Bearer token support on non-OCS endpoints (like Notes API). See [Upstream Status](oauth-upstream-status.md) for details.
|
||||
|
||||
### Benefits
|
||||
- **Zero-config deployment** via dynamic client registration
|
||||
- **No credential storage** in environment variables
|
||||
- **Per-user authentication** with access tokens
|
||||
- **Per-user permissions** - each user has their own Nextcloud client
|
||||
- **Automatic token validation** via Nextcloud OIDC userinfo endpoint
|
||||
- **Token caching** for performance (default: 1 hour TTL)
|
||||
- **PKCE required** for enhanced security (S256 code challenge)
|
||||
- **Secure by design** following OAuth 2.0 and OpenID Connect standards
|
||||
|
||||
### Current Implementation Limitations
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Tested Configuration:**
|
||||
> - ✅ Nextcloud `oidc` app (OIDC Identity Provider) + `user_oidc` app (OIDC User Backend)
|
||||
> - ✅ Nextcloud acting as its own identity provider (self-hosted OIDC)
|
||||
> - ✅ MCP server as OAuth Resource Server
|
||||
> - ✅ PKCE with S256 code challenge method
|
||||
>
|
||||
> **Not Tested:**
|
||||
> - ❌ External identity providers (Azure AD, Keycloak, Okta, etc.)
|
||||
> - ❌ Using `user_oidc` with external OIDC providers
|
||||
>
|
||||
> **Known Requirements:**
|
||||
> - 🔧 The `user_oidc` app requires a patch for Bearer token support on non-OCS endpoints (see [Upstream Status](oauth-upstream-status.md))
|
||||
> - ⏱️ Dynamic client registration credentials expire (default: 1 hour) - use pre-configured clients for production
|
||||
> - 🔐 PKCE must be advertised in OIDC discovery (see [Upstream Status](oauth-upstream-status.md))
|
||||
|
||||
### How OAuth Works
|
||||
|
||||
The MCP server implements the OAuth 2.0 Resource Server pattern:
|
||||
|
||||
**Phase 1: Authorization (OAuth Flow with PKCE)**
|
||||
1. MCP client connects and receives OAuth settings (issuer URL, scopes)
|
||||
2. Client initiates OAuth flow with PKCE (Proof Key for Code Exchange)
|
||||
3. User authenticates via browser to Nextcloud
|
||||
4. Nextcloud redirects back with authorization code
|
||||
5. Client exchanges code + code_verifier for access token
|
||||
|
||||
**Phase 2: API Access (Bearer Token Validation)**
|
||||
6. Client sends MCP requests with `Authorization: Bearer <token>` header
|
||||
7. MCP server validates token by calling Nextcloud's userinfo endpoint
|
||||
8. Server creates per-user NextcloudClient instance with the token
|
||||
9. All Nextcloud API requests use the user's Bearer token
|
||||
10. User-specific permissions and audit trails apply
|
||||
|
||||
This ensures:
|
||||
- Each user has their own authenticated session
|
||||
- Actions appear from the correct user in Nextcloud logs
|
||||
- Proper permission boundaries are maintained
|
||||
- No shared credentials between users
|
||||
|
||||
### See Also
|
||||
- [OAuth Quick Start](quickstart-oauth.md) - 5-minute setup for development
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Detailed production setup
|
||||
- [OAuth Architecture](oauth-architecture.md) - Technical details
|
||||
- [Upstream Status](oauth-upstream-status.md) - Required patches and PR status
|
||||
- [OAuth Troubleshooting](oauth-troubleshooting.md) - OAuth-specific issues
|
||||
- [Configuration](configuration.md) - Environment variables
|
||||
|
||||
## Basic Authentication (Legacy)
|
||||
|
||||
Basic Authentication uses username and password credentials directly.
|
||||
|
||||
### Benefits
|
||||
- **Simple setup** with username/password
|
||||
- **Single-user** server instances
|
||||
- **Quick for development** and testing
|
||||
|
||||
### Limitations
|
||||
- **Credentials in environment** (less secure)
|
||||
- **Single user only** - all requests use the same account
|
||||
- **No audit trail** - all actions appear from the same user
|
||||
- **Maintained for compatibility** - will be deprecated in future versions
|
||||
|
||||
> [!WARNING]
|
||||
> **Security Notice:** Basic Authentication stores credentials in environment variables and is less secure than OAuth. It's maintained for backward compatibility only and may be deprecated in future versions. Use OAuth for production deployments.
|
||||
|
||||
### See Also
|
||||
- [Configuration](configuration.md#basic-authentication-legacy) - BasicAuth environment variables
|
||||
- [Running the Server](running.md#basicauth-mode-legacy) - BasicAuth examples
|
||||
|
||||
## Hybrid Authentication (Multi-User BasicAuth + OAuth)
|
||||
|
||||
When running in multi-user BasicAuth mode with `ENABLE_OFFLINE_ACCESS=true`, the server operates in **hybrid authentication mode**. This provides the simplicity of BasicAuth for normal operations with the security of OAuth for administrative functions.
|
||||
|
||||
### Authentication Domains
|
||||
|
||||
**MCP Operations** (Tools, Resources):
|
||||
- **Auth Method**: BasicAuth (HTTP Basic username/password)
|
||||
- **Characteristics**:
|
||||
- Stateless - no token storage
|
||||
- Simple configuration
|
||||
- Direct credential validation against Nextcloud
|
||||
- Credentials passed per-request in Authorization header
|
||||
- **Used For**: MCP tool calls from Claude, MCP client operations
|
||||
|
||||
**Management APIs** (Webhooks, Admin UI):
|
||||
- **Auth Method**: OAuth bearer tokens
|
||||
- **Characteristics**:
|
||||
- Per-user authorization via OAuth consent flow
|
||||
- Refresh tokens stored for background operations
|
||||
- Token validation via UnifiedTokenVerifier
|
||||
- Explicit user consent required
|
||||
- **Used For**: Astrolabe admin UI, webhook management, vector sync operations
|
||||
One set of credentials is configured in the environment and shared by all MCP clients. All Nextcloud requests are made as the same user.
|
||||
|
||||
### Configuration
|
||||
|
||||
```env
|
||||
# Enable multi-user BasicAuth
|
||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
||||
|
||||
# Enable hybrid mode (OAuth provisioning for management APIs)
|
||||
ENABLE_OFFLINE_ACCESS=true
|
||||
|
||||
# Enable background sync (required for hybrid mode currently)
|
||||
VECTOR_SYNC_ENABLED=true
|
||||
|
||||
# Encryption key for refresh token storage
|
||||
TOKEN_ENCRYPTION_KEY=<base64-encoded-key>
|
||||
|
||||
# Nextcloud connection
|
||||
NEXTCLOUD_HOST=https://cloud.example.com
|
||||
|
||||
# OAuth credentials (optional - uses DCR if not set)
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=<client-id>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<client-secret>
|
||||
```bash
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.example.com
|
||||
NEXTCLOUD_USERNAME=your_username
|
||||
NEXTCLOUD_PASSWORD=your_app_password
|
||||
```
|
||||
|
||||
### OAuth Provisioning Flow
|
||||
|
||||
1. Admin opens Astrolabe admin settings in Nextcloud
|
||||
2. Clicks "Authorize" to enable webhook management
|
||||
3. Redirected to `/oauth/authorize-nextcloud` on MCP server
|
||||
4. MCP server redirects to Nextcloud OAuth consent page
|
||||
5. Admin grants OAuth consent (scopes: `openid`, `profile`, `offline_access`)
|
||||
6. Redirected back to `/oauth/callback` on MCP server
|
||||
7. MCP server stores refresh token (encrypted)
|
||||
8. Admin can now manage webhooks from Astrolabe UI
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Simple MCP client setup**: Use BasicAuth (no OAuth complexity for end users)
|
||||
- **Secure background operations**: Webhooks use per-user OAuth tokens (no shared credentials)
|
||||
- **Explicit authorization**: Admins must explicitly grant OAuth consent for webhook operations
|
||||
- **Per-user isolation**: Each admin's webhook operations use their own refresh token
|
||||
Generate the app password in Nextcloud under **Settings → Security → Devices & sessions**. Don't use your login password.
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- **Two auth systems**: More complex server configuration than pure BasicAuth or OAuth
|
||||
- **OAuth setup required**: Admins must complete OAuth flow before managing webhooks
|
||||
- **Token storage**: Requires database and encryption key for refresh tokens
|
||||
- ✅ Simplest setup; no persistent state
|
||||
- ✅ All MCP tools available (no scope enforcement — trusted environment)
|
||||
- ❌ No per-user identity; all actions appear from the same account in Nextcloud
|
||||
- ❌ Not suitable for shared deployments
|
||||
|
||||
### Comparison
|
||||
See [Configuration](configuration.md) for the full environment variable reference.
|
||||
|
||||
| Feature | Pure BasicAuth | Hybrid Mode | Pure OAuth |
|
||||
|---------|---------------|-------------|------------|
|
||||
| MCP Operations | BasicAuth | BasicAuth | OAuth Bearer Token |
|
||||
| Management API | N/A | OAuth Bearer Token | OAuth Bearer Token |
|
||||
| Webhook Operations | N/A | OAuth Refresh Token | OAuth Refresh Token |
|
||||
| MCP Client Setup | Simple | Simple | Complex (PKCE flow) |
|
||||
| Admin UI Auth | N/A | OAuth Consent | OAuth Login |
|
||||
| Token Storage | None | Refresh tokens only | All tokens |
|
||||
| Deployment Complexity | Low | Medium | High |
|
||||
## Multi-User (BasicAuth Pass-Through)
|
||||
|
||||
### Astrolabe User Setup (Hybrid Mode)
|
||||
Each MCP client sends its own credentials in an HTTP `Authorization: Basic` header. The server creates a per-request Nextcloud client from those credentials and never persists them.
|
||||
|
||||
For Astrolabe-specific user setup instructions in hybrid mode, see the [Astrolabe documentation](https://github.com/cbcoutinho/astrolabe/blob/master/docs/user-setup-hybrid-mode.md).
|
||||
### Configuration
|
||||
|
||||
### See Also
|
||||
- [OAuth Architecture](oauth-architecture.md) - Progressive Consent (Flow 2) details
|
||||
- [Configuration](configuration.md#enable_offline_access) - Hybrid mode configuration
|
||||
```bash
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.example.com
|
||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
||||
```
|
||||
|
||||
`NEXTCLOUD_USERNAME` and `NEXTCLOUD_PASSWORD` must NOT be set in this mode.
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- ✅ Stateless — no token storage required
|
||||
- ✅ Each user's actions are properly attributed in Nextcloud audit logs
|
||||
- ❌ Clients must handle Nextcloud credentials directly (credential exposure risk)
|
||||
- ❌ Not compatible with OAuth-based MCP clients (claude.ai, Astrolabe Cloud) without a credential bridge
|
||||
|
||||
## Multi-User (Login Flow v2)
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
The MCP server enforces `mcp:*` scopes 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.**
|
||||
|
||||
## Mode Detection
|
||||
|
||||
The server automatically detects the authentication mode:
|
||||
The server detects the active mode from environment variables at startup:
|
||||
|
||||
- **OAuth mode**: When `NEXTCLOUD_USERNAME` and `NEXTCLOUD_PASSWORD` are NOT set
|
||||
- **BasicAuth mode**: When both username and password are provided
|
||||
| Env vars present | Detected mode |
|
||||
|------------------|---------------|
|
||||
| `NEXTCLOUD_USERNAME` + `NEXTCLOUD_PASSWORD` | Single-User (BasicAuth) |
|
||||
| `ENABLE_MULTI_USER_BASIC_AUTH=true` (no creds) | Multi-User (BasicAuth pass-through) |
|
||||
| `ENABLE_LOGIN_FLOW=true` (no creds) | Multi-User (Login Flow v2) |
|
||||
|
||||
You can also force a mode via CLI flag:
|
||||
|
||||
You can also force a specific mode using CLI flags:
|
||||
```bash
|
||||
# Force OAuth mode
|
||||
# Force Login Flow v2 / OAuth identity layer
|
||||
uv run nextcloud-mcp-server --oauth
|
||||
|
||||
# Force BasicAuth mode
|
||||
# Force BasicAuth
|
||||
uv run nextcloud-mcp-server --no-oauth
|
||||
```
|
||||
|
||||
## Switching Between Modes
|
||||
## See Also
|
||||
|
||||
See [Troubleshooting: Switching Between OAuth and BasicAuth](troubleshooting.md#switching-between-oauth-and-basicauth) for instructions.
|
||||
- [Login Flow v2](login-flow-v2.md) — multi-user setup details
|
||||
- [Configuration](configuration.md) — environment variable reference
|
||||
- [Authentication Flows](auth-flows.md) — sequence diagrams per mode
|
||||
- [Running the Server](running.md) — start, manage, troubleshoot
|
||||
- [Troubleshooting](troubleshooting.md) — common issues
|
||||
- [ADR-022](ADR-022-deployment-mode-consolidation.md) — design rationale for mode consolidation
|
||||
|
||||
+66
-126
@@ -30,144 +30,86 @@ Then choose your deployment mode:
|
||||
|
||||
## Deployment Mode Selection
|
||||
|
||||
**New in v0.58.0:** You can explicitly declare your deployment mode to remove ambiguity and catch configuration errors early.
|
||||
The server supports three deployment modes. See [Authentication](authentication.md) for the full comparison and [Login Flow v2](login-flow-v2.md) for the recommended multi-user setup.
|
||||
|
||||
| Mode | When to use |
|
||||
|------|-------------|
|
||||
| `single_user_basic` | Personal use, dev — credentials in env vars |
|
||||
| `multi_user_basic` | Internal deployments — clients send credentials via `Authorization: Basic` header |
|
||||
| `login_flow_v2` | Hosted / OAuth-based MCP clients (claude.ai, Astrolabe Cloud) — recommended for multi-user |
|
||||
|
||||
You can declare the mode explicitly:
|
||||
|
||||
```dotenv
|
||||
# Optional but recommended
|
||||
MCP_DEPLOYMENT_MODE=oauth_single_audience
|
||||
MCP_DEPLOYMENT_MODE=login_flow_v2
|
||||
```
|
||||
|
||||
**Valid values:**
|
||||
- `single_user_basic` - Single-user with username/password
|
||||
- `multi_user_basic` - Multi-user with BasicAuth pass-through
|
||||
- `oauth_single_audience` - Multi-user OAuth (recommended)
|
||||
- `oauth_token_exchange` - Multi-user OAuth with token exchange
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Clear which mode is active
|
||||
- ✅ Better validation error messages
|
||||
- ✅ Self-documenting configuration
|
||||
- ✅ Catches configuration mistakes early
|
||||
|
||||
**Auto-detection:** If `MCP_DEPLOYMENT_MODE` is not set, the server auto-detects the mode based on other settings (existing behavior).
|
||||
|
||||
See [Authentication Modes](authentication.md) for detailed comparison of deployment modes.
|
||||
If `MCP_DEPLOYMENT_MODE` is not set, the server auto-detects from the other env vars below.
|
||||
|
||||
---
|
||||
|
||||
## Single-User BasicAuth Mode
|
||||
|
||||
BasicAuth with a single user is the simplest deployment mode. Use for personal instances, local development, and testing.
|
||||
The simplest mode. Use for personal instances, local development, and testing.
|
||||
|
||||
```dotenv
|
||||
# Minimal single-user configuration
|
||||
NEXTCLOUD_HOST=http://localhost:8080
|
||||
NEXTCLOUD_USERNAME=admin
|
||||
NEXTCLOUD_PASSWORD=password
|
||||
|
||||
# Optional: Explicit mode declaration
|
||||
MCP_DEPLOYMENT_MODE=single_user_basic
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> **Security Notice:** BasicAuth stores credentials in environment variables and is less secure than OAuth. Use OAuth for production multi-user deployments.
|
||||
|
||||
---
|
||||
|
||||
## Multi-User OAuth Modes
|
||||
|
||||
OAuth2/OIDC is the recommended authentication mode for production multi-user deployments.
|
||||
|
||||
### Minimal Configuration (Auto-registration)
|
||||
|
||||
```dotenv
|
||||
# .env file for OAuth with auto-registration
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
|
||||
# Optional: Explicit mode declaration (recommended)
|
||||
MCP_DEPLOYMENT_MODE=oauth_single_audience
|
||||
|
||||
# Leave these EMPTY for OAuth mode
|
||||
NEXTCLOUD_USERNAME=
|
||||
NEXTCLOUD_PASSWORD=
|
||||
```
|
||||
|
||||
This minimal configuration uses dynamic client registration to automatically register an OAuth client at startup.
|
||||
|
||||
### Full Configuration (Pre-configured Client)
|
||||
|
||||
```dotenv
|
||||
# .env file for OAuth with pre-configured client
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
|
||||
# Optional: Explicit mode declaration (recommended)
|
||||
MCP_DEPLOYMENT_MODE=oauth_single_audience
|
||||
|
||||
# OAuth Client Credentials (optional - auto-registers if not provided)
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=your-client-id
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=your-client-secret
|
||||
|
||||
# OAuth Callback Settings (optional)
|
||||
NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000
|
||||
|
||||
# Leave these EMPTY for OAuth mode
|
||||
NEXTCLOUD_USERNAME=
|
||||
NEXTCLOUD_PASSWORD=
|
||||
```
|
||||
|
||||
### Environment Variables Reference
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `NEXTCLOUD_HOST` | ✅ Yes | - | Full URL of your Nextcloud instance (e.g., `https://cloud.example.com`) |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_ID` | ⚠️ Optional | - | OAuth client ID (auto-registers if empty) |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_SECRET` | ⚠️ Optional | - | OAuth client secret (auto-registers if empty) |
|
||||
| `NEXTCLOUD_MCP_SERVER_URL` | ⚠️ Optional | `http://localhost:8000` | MCP server URL for OAuth callbacks |
|
||||
| `OIDC_RESOURCE_SERVER_ID` | ⚠️ Optional | - | Resource server identifier for IdPs that require prefixed scopes (e.g., AWS Cognito). When set, resource scopes are sent as `{id}/{scope}` |
|
||||
| `NEXTCLOUD_USERNAME` | ❌ Must be empty | - | Leave empty to enable OAuth mode |
|
||||
| `NEXTCLOUD_PASSWORD` | ❌ Must be empty | - | Leave empty to enable OAuth mode |
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before using OAuth configuration:
|
||||
|
||||
1. **Install required Nextcloud apps** (both are required):
|
||||
- **`oidc`** - OIDC Identity Provider (Apps → Security)
|
||||
- **`user_oidc`** - OpenID Connect user backend (Apps → Security)
|
||||
|
||||
2. **Configure the apps**:
|
||||
- Enable dynamic client registration (if using auto-registration) - Settings → OIDC
|
||||
- Enable Bearer token validation: `php occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean`
|
||||
|
||||
3. **Apply Bearer token patch** - The `user_oidc` app requires a patch for non-OCS endpoints - See [Upstream Status](oauth-upstream-status.md) for details
|
||||
|
||||
See the [OAuth Setup Guide](oauth-setup.md) for detailed step-by-step instructions, or [OAuth Quick Start](quickstart-oauth.md) for a 5-minute setup.
|
||||
|
||||
---
|
||||
|
||||
## Basic Authentication (Legacy)
|
||||
|
||||
Basic Authentication is maintained for backward compatibility. It uses username and password credentials.
|
||||
|
||||
> [!WARNING]
|
||||
> **Security Notice:** Basic Authentication stores credentials in environment variables and is less secure than OAuth. Use OAuth for production deployments.
|
||||
|
||||
### Configuration
|
||||
|
||||
```dotenv
|
||||
# .env file for BasicAuth mode
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
NEXTCLOUD_USERNAME=your_nextcloud_username
|
||||
NEXTCLOUD_PASSWORD=your_app_password_or_password
|
||||
NEXTCLOUD_PASSWORD=your_app_password
|
||||
```
|
||||
|
||||
### Environment Variables Reference
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `NEXTCLOUD_HOST` | ✅ Yes | Full URL of your Nextcloud instance |
|
||||
| `NEXTCLOUD_USERNAME` | ✅ Yes | Your Nextcloud username |
|
||||
| `NEXTCLOUD_PASSWORD` | ✅ Yes | **Recommended:** Use a dedicated [Nextcloud App Password](https://docs.nextcloud.com/server/latest/user_manual/en/session_management.html#managing-devices). Generate one in Nextcloud Security settings. Alternatively, use your login password (less secure). |
|
||||
| `NEXTCLOUD_PASSWORD` | ✅ Yes | Use a dedicated [Nextcloud app password](https://docs.nextcloud.com/server/latest/user_manual/en/session_management.html#managing-devices), not your login password |
|
||||
|
||||
---
|
||||
|
||||
## Multi-User BasicAuth Mode
|
||||
|
||||
Each MCP client sends its own Nextcloud credentials in an `Authorization: Basic` header. The server passes them through per-request and never persists them.
|
||||
|
||||
```dotenv
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
ENABLE_MULTI_USER_BASIC_AUTH=true
|
||||
|
||||
# Optional: enable per-user app-password storage for background sync
|
||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||
TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
```
|
||||
|
||||
`NEXTCLOUD_USERNAME` and `NEXTCLOUD_PASSWORD` must NOT be set in this mode.
|
||||
|
||||
---
|
||||
|
||||
## Login Flow v2 Mode
|
||||
|
||||
The recommended multi-user mode. MCP clients authenticate to the MCP server via OAuth; the server holds per-user Nextcloud app passwords (encrypted) obtained via Login Flow v2.
|
||||
|
||||
```dotenv
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
ENABLE_LOGIN_FLOW=true
|
||||
|
||||
# App-password storage (required)
|
||||
TOKEN_ENCRYPTION_KEY=<fernet-key>
|
||||
TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
|
||||
# Public URLs for browser redirects
|
||||
NEXTCLOUD_MCP_SERVER_URL=https://mcp.example.com
|
||||
NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.instance.com
|
||||
```
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `NEXTCLOUD_HOST` | ✅ Yes | Internal URL of your Nextcloud instance (server-to-server) |
|
||||
| `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_PUBLIC_ISSUER_URL` | ✅ Yes | Public URL of Nextcloud (for browser redirects during Login Flow v2) |
|
||||
|
||||
See [Login Flow v2](login-flow-v2.md) for full setup, scope reference, and troubleshooting.
|
||||
|
||||
---
|
||||
|
||||
@@ -247,10 +189,11 @@ QDRANT_LOCATION=:memory:
|
||||
OLLAMA_BASE_URL=http://ollama:11434
|
||||
```
|
||||
|
||||
**Multi-User OAuth Mode:**
|
||||
**Multi-User Login Flow v2 Mode:**
|
||||
```dotenv
|
||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||
MCP_DEPLOYMENT_MODE=oauth_single_audience
|
||||
MCP_DEPLOYMENT_MODE=login_flow_v2
|
||||
ENABLE_LOGIN_FLOW=true
|
||||
|
||||
# Enable semantic search
|
||||
# In multi-user modes, this AUTOMATICALLY enables background operations!
|
||||
@@ -710,12 +653,9 @@ uv run nextcloud-mcp-server --no-oauth \
|
||||
## See Also
|
||||
|
||||
- [Configuration Migration Guide v2](configuration-migration-v2.md) - **New in v0.58.0:** Migrate from old variable names
|
||||
- [OAuth Quick Start](quickstart-oauth.md) - 5-minute OAuth setup for development
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Detailed OAuth configuration for production
|
||||
- [OAuth Architecture](oauth-architecture.md) - How OAuth works in the MCP server
|
||||
- [Upstream Status](oauth-upstream-status.md) - Required patches and upstream PRs
|
||||
- [Authentication](authentication.md) - Authentication modes comparison
|
||||
- [Login Flow v2](login-flow-v2.md) - Recommended multi-user setup
|
||||
- [Running the Server](running.md) - Starting the server with different configurations
|
||||
- [Troubleshooting](troubleshooting.md) - Common configuration issues
|
||||
- [OAuth Troubleshooting](oauth-troubleshooting.md) - OAuth-specific troubleshooting
|
||||
- [ADR-021](ADR-021-configuration-consolidation.md) - Configuration consolidation architecture decision
|
||||
- [ADR-022](ADR-022-deployment-mode-consolidation.md) - Deployment mode consolidation
|
||||
|
||||
@@ -132,7 +132,7 @@ docker-compose up -d
|
||||
After installation:
|
||||
|
||||
1. **Configure the server** - See [Configuration Guide](configuration.md)
|
||||
2. **Set up authentication** - See [OAuth Setup Guide](oauth-setup.md) or [Authentication](authentication.md)
|
||||
2. **Set up authentication** - See [Authentication](authentication.md) (multi-user deployments: see [Login Flow v2](login-flow-v2.md))
|
||||
3. **Run the server** - See [Running the Server](running.md)
|
||||
|
||||
## Updating
|
||||
@@ -211,5 +211,6 @@ sudo docker run ...
|
||||
## See Also
|
||||
|
||||
- [Configuration Guide](configuration.md) - Environment variables and settings
|
||||
- [OAuth Setup Guide](oauth-setup.md) - OAuth authentication setup
|
||||
- [Authentication](authentication.md) - Authentication modes comparison
|
||||
- [Login Flow v2](login-flow-v2.md) - Recommended multi-user setup
|
||||
- [Running the Server](running.md) - Starting and managing the server
|
||||
|
||||
@@ -1,898 +0,0 @@
|
||||
# JWT OAuth Reference - Nextcloud MCP Server
|
||||
|
||||
**Last Updated:** 2025-10-23
|
||||
**Status:** Production Ready
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [JWT vs Opaque Tokens](#jwt-vs-opaque-tokens)
|
||||
- [Scope-Based Authorization](#scope-based-authorization)
|
||||
- [Configuration](#configuration)
|
||||
- [Architecture](#architecture)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Production Deployment](#production-deployment)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Nextcloud MCP Server supports OAuth authentication with both **JWT** (RFC 9068) and **opaque** tokens. JWT tokens are recommended for production use as they enable:
|
||||
|
||||
- **Faster validation** - No HTTP call needed for token verification
|
||||
- **Direct scope extraction** - Scopes embedded in token claims
|
||||
- **Dynamic tool filtering** - Users only see tools they have permission to use
|
||||
- **Signature verification** - Cryptographic validation using JWKS
|
||||
|
||||
### Key Features
|
||||
|
||||
- ✅ **JWT Token Support** - RFC 9068 compliant access tokens with RS256 signatures
|
||||
- ✅ **Custom Scopes** - `mcp:notes.read` and `mcp:notes.write` for read/write access control
|
||||
- ✅ **Dynamic Tool Filtering** - Tools filtered based on user's token scopes
|
||||
- ✅ **Scope Challenges** - RFC-compliant `WWW-Authenticate` headers for insufficient scopes
|
||||
- ✅ **Protected Resource Metadata** - RFC 9728 endpoint for scope discovery
|
||||
- ✅ **Backward Compatible** - BasicAuth mode bypasses all scope checks
|
||||
|
||||
### Supported Scopes
|
||||
|
||||
| Scope | Description | Tool Count |
|
||||
|-------|-------------|------------|
|
||||
| `mcp:notes.read` | Read-only access to Nextcloud data | 36 tools |
|
||||
| `mcp:notes.write` | Write access to create/modify/delete data | 54 tools |
|
||||
|
||||
All MCP tools (90 total) require at least one of these scopes. Standard OIDC scopes (`openid`, `profile`, `email`) are also supported.
|
||||
|
||||
---
|
||||
|
||||
## JWT vs Opaque Tokens
|
||||
|
||||
The Nextcloud OIDC app supports two token formats, configured per-client:
|
||||
|
||||
### JWT Tokens (Recommended)
|
||||
|
||||
**Advantages:**
|
||||
- ✅ Fast validation - JWT signature verified locally using JWKS
|
||||
- ✅ Direct scope extraction from `scope` claim in payload
|
||||
- ✅ Standard approach (RFC 9068)
|
||||
- ✅ No additional HTTP calls for validation
|
||||
|
||||
**Disadvantages:**
|
||||
- ⚠️ Larger size (~800-1200 chars vs 72 chars for opaque)
|
||||
- ⚠️ Token payload visible to client (not an issue for access tokens)
|
||||
|
||||
**Token Structure:**
|
||||
```json
|
||||
{
|
||||
"header": {
|
||||
"typ": "at+JWT",
|
||||
"alg": "RS256",
|
||||
"kid": "..."
|
||||
},
|
||||
"payload": {
|
||||
"iss": "http://localhost:8080",
|
||||
"sub": "admin",
|
||||
"aud": "client_id",
|
||||
"exp": 1234567890,
|
||||
"iat": 1234567890,
|
||||
"scope": "openid profile email mcp:notes.read mcp:notes.write",
|
||||
"client_id": "...",
|
||||
"jti": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Opaque Tokens
|
||||
|
||||
**Advantages:**
|
||||
- ✅ Smaller size (72 characters)
|
||||
- ✅ No payload visible to client
|
||||
- ✅ Direct scope access via introspection endpoint (RFC 7662)
|
||||
|
||||
**Disadvantages:**
|
||||
- ❌ Higher latency - Requires HTTP call to introspection endpoint
|
||||
- ❌ Slower than JWT signature verification (network roundtrip)
|
||||
|
||||
**Validation Method:**
|
||||
Opaque tokens are validated using the **introspection endpoint** (`/apps/oidc/introspect`), which returns:
|
||||
- Token active status
|
||||
- Scope claim (direct access, no inference needed)
|
||||
- User information (`sub`, `username`)
|
||||
- Token metadata (`exp`, `iat`, `client_id`)
|
||||
|
||||
Falls back to userinfo endpoint only if introspection is unavailable.
|
||||
|
||||
**When to Use:**
|
||||
- Use **JWT tokens** for production (better performance, no HTTP call)
|
||||
- Use **opaque tokens** for compatibility with clients that don't support JWT
|
||||
|
||||
---
|
||||
|
||||
## Scope-Based Authorization
|
||||
|
||||
### Scope Definitions
|
||||
|
||||
The MCP server uses **coarse-grained scopes** for simplicity:
|
||||
|
||||
| Scope | Operations | Examples |
|
||||
|-------|------------|----------|
|
||||
| `mcp:notes.read` | Read-only access | Get notes, search files, list calendars, read contacts |
|
||||
| `mcp:notes.write` | Write operations | Create notes, update events, delete files, modify contacts |
|
||||
|
||||
### Standard OIDC Scopes
|
||||
|
||||
| Scope | Description | Required |
|
||||
|-------|-------------|----------|
|
||||
| `openid` | OIDC authentication | Yes |
|
||||
| `profile` | User profile information | Recommended |
|
||||
| `email` | Email address | Recommended |
|
||||
|
||||
### Recommended Configurations
|
||||
|
||||
**Full Access:**
|
||||
```
|
||||
openid profile email mcp:notes.read mcp:notes.write
|
||||
```
|
||||
|
||||
**Read-Only:**
|
||||
```
|
||||
openid profile email mcp:notes.read
|
||||
```
|
||||
|
||||
**No Custom Scopes (OIDC only):**
|
||||
```
|
||||
openid profile email
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
All 90 MCP tools are decorated with scope requirements:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
@require_scopes("mcp:notes.read")
|
||||
async def nc_notes_get_note(note_id: int, ctx: Context):
|
||||
"""Get a note by ID (requires mcp:notes.read scope)"""
|
||||
...
|
||||
|
||||
@mcp.tool()
|
||||
@require_scopes("mcp:notes.write")
|
||||
async def nc_notes_create_note(title: str, content: str, ctx: Context):
|
||||
"""Create a note (requires mcp:notes.write scope)"""
|
||||
...
|
||||
```
|
||||
|
||||
**Coverage:**
|
||||
- ✅ 36 read tools decorated with `@require_scopes("mcp:notes.read")`
|
||||
- ✅ 54 write tools decorated with `@require_scopes("mcp:notes.write")`
|
||||
- ✅ 90/90 tools covered (100%)
|
||||
|
||||
### Dynamic Tool Filtering
|
||||
|
||||
The MCP server implements **dynamic tool filtering** - users only see tools they have permission to use. This applies to **both JWT and Bearer (opaque) tokens** in OAuth mode:
|
||||
|
||||
**Token with `mcp:notes.read` only:**
|
||||
- `list_tools()` returns 36 read-only tools
|
||||
- Write tools are hidden from the tool list
|
||||
|
||||
**Token with `mcp:notes.write` only:**
|
||||
- `list_tools()` returns 54 write-only tools
|
||||
- Read tools are hidden from the tool list
|
||||
|
||||
**Token with both scopes:**
|
||||
- `list_tools()` returns all 90 tools
|
||||
|
||||
**Token with no custom scopes:**
|
||||
- `list_tools()` returns 0 tools (all require `mcp:notes.read` or `mcp:notes.write`)
|
||||
|
||||
**BasicAuth mode:**
|
||||
- `list_tools()` returns all 90 tools (no filtering)
|
||||
|
||||
**Note:** JWT tokens include scopes in the token payload, while Bearer tokens retrieve scopes via the introspection endpoint. Both methods provide reliable scope information for filtering.
|
||||
|
||||
### Scope Challenges
|
||||
|
||||
When a tool is called without required scopes, the server returns a `403 Forbidden` response with a `WWW-Authenticate` header:
|
||||
|
||||
```http
|
||||
HTTP/1.1 403 Forbidden
|
||||
WWW-Authenticate: Bearer error="insufficient_scope",
|
||||
scope="mcp:notes.write",
|
||||
resource_metadata="http://server/.well-known/oauth-protected-resource/mcp"
|
||||
```
|
||||
|
||||
This enables **step-up authorization** - clients can detect missing scopes and trigger re-authentication to obtain additional permissions.
|
||||
|
||||
### Protected Resource Metadata (PRM)
|
||||
|
||||
The server implements RFC 9728's Protected Resource Metadata endpoint:
|
||||
|
||||
**Endpoint:** `GET /.well-known/oauth-protected-resource/mcp`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"resource": "http://localhost:8001/mcp",
|
||||
"scopes_supported": ["mcp:notes.read", "mcp:notes.write"],
|
||||
"authorization_servers": ["http://localhost:8080"],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"resource_signing_alg_values_supported": ["RS256"]
|
||||
}
|
||||
```
|
||||
|
||||
This allows OAuth clients to discover supported scopes before requesting authorization.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Docker Services
|
||||
|
||||
The development environment includes two MCP server variants:
|
||||
|
||||
| Service | Port | Auth Type | Token Type | Use Case |
|
||||
|---------|------|-----------|------------|----------|
|
||||
| `mcp` | 8000 | BasicAuth | N/A | Development, testing |
|
||||
| `mcp-oauth` | 8001 | OAuth | JWT (configurable) | OAuth testing with JWT tokens |
|
||||
|
||||
### OAuth Service Configuration
|
||||
|
||||
The `mcp-oauth` service uses **Dynamic Client Registration (DCR)** by default and is configured to request JWT tokens:
|
||||
|
||||
**Default Configuration (DCR with JWT tokens):**
|
||||
```yaml
|
||||
mcp-oauth:
|
||||
build: .
|
||||
command: ["--transport", "streamable-http", "--oauth", "--port", "8001", "--oauth-token-type", "jwt"]
|
||||
ports:
|
||||
- 127.0.0.1:8001:8001
|
||||
environment:
|
||||
- NEXTCLOUD_HOST=http://app:80
|
||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8001
|
||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||
- NEXTCLOUD_OIDC_SCOPES=openid profile email mcp:notes.read mcp:notes.write
|
||||
volumes:
|
||||
- oauth-client-storage:/app/.oauth # Persist DCR credentials
|
||||
```
|
||||
|
||||
**With Pre-Configured Credentials:**
|
||||
```yaml
|
||||
mcp-oauth:
|
||||
build: .
|
||||
command: ["--transport", "streamable-http", "--oauth", "--port", "8001", "--oauth-token-type", "jwt"]
|
||||
ports:
|
||||
- 127.0.0.1:8001:8001
|
||||
environment:
|
||||
- NEXTCLOUD_HOST=http://app:80
|
||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8001
|
||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||
- NEXTCLOUD_OIDC_CLIENT_ID=<your_client_id> # Skips DCR
|
||||
- NEXTCLOUD_OIDC_CLIENT_SECRET=<your_client_secret> # Skips DCR
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- **No credentials needed** - DCR automatically registers the client on first start
|
||||
- **Credentials persist** - Saved to SQLite database and reused
|
||||
- **JWT tokens** - Use `--oauth-token-type jwt` for better performance
|
||||
- **Token verifier supports both** - Can handle JWT and opaque tokens
|
||||
- **Pre-configured credentials** - Providing `CLIENT_ID`/`CLIENT_SECRET` skips DCR
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `NEXTCLOUD_HOST` | Nextcloud base URL | `http://localhost:8080` |
|
||||
| `NEXTCLOUD_MCP_SERVER_URL` | MCP server external URL for OAuth callbacks | (required in OAuth mode) |
|
||||
| `NEXTCLOUD_PUBLIC_ISSUER_URL` | Public issuer URL for JWT validation | (uses `NEXTCLOUD_HOST`) |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_ID` | Pre-configured OAuth client ID | (optional - uses DCR if unset) |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_SECRET` | Pre-configured OAuth client secret | (optional - uses DCR if unset) |
|
||||
| `NEXTCLOUD_OIDC_SCOPES` | Space-separated scopes to request | `"openid profile email mcp:notes.read mcp:notes.write"` |
|
||||
| `NEXTCLOUD_OIDC_TOKEN_TYPE` | Token format: `"jwt"` or `"Bearer"` | `"Bearer"` |
|
||||
|
||||
### Dynamic Client Registration (DCR)
|
||||
|
||||
The MCP server supports **automatic OAuth client registration** using the OIDC Discovery registration endpoint. This eliminates the need for manual client creation in most cases.
|
||||
|
||||
**How It Works:**
|
||||
|
||||
When the MCP server starts in OAuth mode, it follows this **three-tier credential loading strategy**:
|
||||
|
||||
```
|
||||
1. Environment Variables (Highest Priority)
|
||||
├─ NEXTCLOUD_OIDC_CLIENT_ID
|
||||
└─ NEXTCLOUD_OIDC_CLIENT_SECRET
|
||||
|
||||
2. SQLite Database (Second Priority)
|
||||
└─ OAuth client credentials table
|
||||
|
||||
3. Dynamic Client Registration (Automatic Fallback)
|
||||
├─ Discovers registration endpoint from /.well-known/openid-configuration
|
||||
├─ Registers new client with requested scopes and token type
|
||||
├─ Saves credentials to storage file for future use
|
||||
└─ Client credentials persist across restarts
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
DCR automatically configures the client based on environment variables:
|
||||
|
||||
```bash
|
||||
# Minimal DCR configuration (no credentials needed!)
|
||||
export NEXTCLOUD_HOST=http://localhost:8080
|
||||
export NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000
|
||||
export NEXTCLOUD_OIDC_SCOPES="openid profile email mcp:notes.read mcp:notes.write"
|
||||
export NEXTCLOUD_OIDC_TOKEN_TYPE=jwt # or "Bearer" for opaque tokens
|
||||
```
|
||||
|
||||
**Credential Storage:**
|
||||
|
||||
- Registered credentials are saved to SQLite database
|
||||
- Database is encrypted and protected by file system permissions
|
||||
- Credentials are reused on subsequent starts (no re-registration needed)
|
||||
- Stored credentials are checked for expiration (auto-regenerates if expired)
|
||||
|
||||
**Format:**
|
||||
```json
|
||||
{
|
||||
"client_id": "XBd2xqIisu3Kswg39Ub4BUhC36PEYjwwivx3G5nZdDgigvwKXrTHozs7m9DeoLSY",
|
||||
"client_secret": "xNKcy0qpUSau36T60pGGdb03pMEVLXtqykxjK8YkDpoNxNcZ4ClyAT3IAEse2AKT",
|
||||
"client_id_issued_at": 1761097039,
|
||||
"client_secret_expires_at": 2076457039,
|
||||
"redirect_uris": ["http://localhost:8000/oauth/callback"]
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Zero-configuration OAuth setup
|
||||
- ✅ Automatic credential management
|
||||
- ✅ Supports both JWT and opaque tokens
|
||||
- ✅ Credentials persist across container restarts
|
||||
- ✅ Automatic re-registration if credentials expire
|
||||
- ✅ Properly sets `allowed_scopes` for JWT token validation
|
||||
|
||||
### Manual Client Creation
|
||||
|
||||
Manual client creation is **optional** but may be preferred when:
|
||||
- You want explicit control over client configuration
|
||||
- You're deploying to production environments with strict security policies
|
||||
- You need to pre-provision OAuth clients before deployment
|
||||
|
||||
**Create Client via OCC Command:**
|
||||
|
||||
```bash
|
||||
docker compose exec app php occ oidc:create \
|
||||
--token_type=jwt \
|
||||
--allowed_scopes="openid profile email mcp:notes.read mcp:notes.write" \
|
||||
"Nextcloud MCP Server" \
|
||||
"http://localhost:8000/oauth/callback"
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"client_id": "XBd2xqIisu3Kswg39Ub4BUhC36PEYjwwivx3G5nZdDgigvwKXrTHozs7m9DeoLSY",
|
||||
"client_secret": "xNKcy0qpUSau36T60pGGdb03pMEVLXtqykxjK8YkDpoNxNcZ4ClyAT3IAEse2AKT",
|
||||
"token_type": "jwt",
|
||||
"allowed_scopes": "openid profile email mcp:notes.read mcp:notes.write"
|
||||
}
|
||||
```
|
||||
|
||||
**Configure MCP Server with Pre-Configured Credentials:**
|
||||
|
||||
```bash
|
||||
# Option 1: Environment variables (highest priority)
|
||||
export NEXTCLOUD_OIDC_CLIENT_ID="<client_id>"
|
||||
export NEXTCLOUD_OIDC_CLIENT_SECRET="<client_secret>"
|
||||
export NEXTCLOUD_OIDC_TOKEN_TYPE="jwt"
|
||||
|
||||
# Option 2: SQLite database (second priority)
|
||||
# Credentials are automatically saved to the database after DCR
|
||||
# Server will automatically load them on startup
|
||||
```
|
||||
|
||||
When credentials are provided via environment variables or storage file, **DCR is skipped**.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Overview
|
||||
|
||||
```
|
||||
┌──────────────────┐ OAuth Flow ┌──────────────────┐
|
||||
│ OAuth Client │<─────────────────────>│ Nextcloud OIDC │
|
||||
│ (Claude, etc) │ │ Server │
|
||||
└────────┬─────────┘ └────────┬─────────┘
|
||||
│ │
|
||||
│ JWT Access Token │
|
||||
│ { │
|
||||
│ "scope": "openid mcp:notes.read mcp:notes.write" │
|
||||
│ ... │
|
||||
│ } │
|
||||
│ │
|
||||
v │
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Nextcloud MCP Server │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ NextcloudTokenVerifier │ │
|
||||
│ │ - JWT signature verification (JWKS) │ │
|
||||
│ │ - Introspection endpoint (opaque tokens) │ │
|
||||
│ │ - Userinfo fallback (last resort) │ │
|
||||
│ └───────────────────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ v │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ Dynamic Tool Filtering (list_tools) │ │
|
||||
│ │ - Get user scopes from verified token │ │
|
||||
│ │ - Filter tools based on @require_scopes metadata │ │
|
||||
│ │ - Return only accessible tools │ │
|
||||
│ └───────────────────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ v │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ Tool Execution (@require_scopes decorator) │ │
|
||||
│ │ - Check token scopes before execution │ │
|
||||
│ │ - Raise InsufficientScopeError if missing │ │
|
||||
│ │ - Return 403 with WWW-Authenticate header │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
**1. Token Verification** (`nextcloud_mcp_server/auth/token_verifier.py`)
|
||||
- **Three-tier validation strategy:**
|
||||
1. **JWT verification** (lines 116-124): JWKS signature validation for JWT tokens
|
||||
2. **Introspection** (lines 126-134): RFC 7662 endpoint for opaque tokens
|
||||
3. **Userinfo fallback** (lines 137-142): Last resort if introspection unavailable
|
||||
- Scope extraction from token payload (JWT) or introspection response (opaque)
|
||||
- Token caching with TTL to reduce repeated validations
|
||||
- Supports both access token formats transparently
|
||||
|
||||
**2. Scope Authorization** (`nextcloud_mcp_server/auth/scope_authorization.py`)
|
||||
- `@require_scopes()` decorator for tools
|
||||
- `get_required_scopes()` - Extract scope requirements from functions
|
||||
- `has_required_scopes()` - Check if user has necessary scopes
|
||||
- `InsufficientScopeError` exception for WWW-Authenticate challenges
|
||||
|
||||
**3. Dynamic Filtering** (`nextcloud_mcp_server/app.py:473-516`)
|
||||
- Overrides FastMCP's `list_tools()` method
|
||||
- Filters based on user's OAuth token scopes (JWT and Bearer)
|
||||
- Only active in OAuth mode
|
||||
- Bypassed in BasicAuth mode
|
||||
|
||||
**4. PRM Endpoint** (`nextcloud_mcp_server/app.py:503-532`)
|
||||
- `GET /.well-known/oauth-protected-resource/mcp`
|
||||
- Advertises `["mcp:notes.read", "mcp:notes.write"]`
|
||||
- RFC 9728 compliant
|
||||
|
||||
**5. Exception Handler** (`nextcloud_mcp_server/app.py:540-563`)
|
||||
- Catches `InsufficientScopeError`
|
||||
- Returns 403 with `WWW-Authenticate` header
|
||||
- Includes missing scopes and PRM endpoint URL
|
||||
|
||||
### Token Validation Flow
|
||||
|
||||
The `NextcloudTokenVerifier` implements a **cascading validation strategy** that handles both JWT and opaque tokens efficiently:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ verify_token(token) │
|
||||
│ (nextcloud_mcp_server/auth/token_verifier.py:88-142) │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
├──> 1. Check cache (lines 106-109)
|
||||
│ ├─ Hit: Return cached AccessToken
|
||||
│ └─ Miss: Continue to validation
|
||||
│
|
||||
├──> 2. JWT Format Check (lines 112-124)
|
||||
│ ├─ Token has 3 parts (header.payload.signature)?
|
||||
│ │ └─ Yes: Attempt JWT verification
|
||||
│ │ ├─ Verify signature with JWKS (RS256)
|
||||
│ │ ├─ Validate issuer, expiration
|
||||
│ │ ├─ Extract scopes from payload
|
||||
│ │ └─ Success: Return AccessToken
|
||||
│ └─ Fail/Not JWT: Continue to introspection
|
||||
│
|
||||
├──> 3. Introspection (lines 126-134)
|
||||
│ ├─ POST to /apps/oidc/introspect
|
||||
│ ├─ Authenticate with client credentials
|
||||
│ ├─ Response contains:
|
||||
│ │ • active: true/false
|
||||
│ │ • scope: "openid mcp:notes.read mcp:notes.write"
|
||||
│ │ • sub, exp, iat, client_id
|
||||
│ ├─ Extract scopes from response
|
||||
│ └─ Success: Return AccessToken
|
||||
│
|
||||
└──> 4. Userinfo Fallback (lines 137-142)
|
||||
├─ GET /apps/oidc/userinfo
|
||||
├─ Bearer token in Authorization header
|
||||
├─ Infer scopes from response claims
|
||||
└─ Return AccessToken or None
|
||||
```
|
||||
|
||||
**Validation Priorities:**
|
||||
|
||||
| Token Type | Method | Performance | Scope Access | Code Reference |
|
||||
|------------|--------|-------------|--------------|----------------|
|
||||
| JWT | JWKS Signature | ⚡ Fastest (local) | Direct (`scope` claim) | `token_verifier.py:156-234` |
|
||||
| Opaque | Introspection | 🔄 Medium (HTTP) | Direct (`scope` field) | `token_verifier.py:236-328` |
|
||||
| Any | Userinfo | 🐌 Slowest (HTTP + inference) | Inferred (from claims) | `token_verifier.py:330-386` |
|
||||
|
||||
**Configuration** (`nextcloud_mcp_server/app.py:391-399`):
|
||||
```python
|
||||
token_verifier = NextcloudTokenVerifier(
|
||||
nextcloud_host=nextcloud_host,
|
||||
userinfo_uri=userinfo_uri,
|
||||
jwks_uri=jwks_uri, # Enables JWT verification
|
||||
issuer=jwt_validation_issuer, # For JWT issuer validation
|
||||
introspection_uri=introspection_uri, # Enables introspection for opaque tokens
|
||||
client_id=client_id, # Required for introspection auth
|
||||
client_secret=client_secret, # Required for introspection auth
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Infrastructure
|
||||
|
||||
The test suite includes comprehensive coverage for JWT OAuth and scope authorization:
|
||||
|
||||
**Test Files:**
|
||||
- `tests/server/test_scope_authorization.py` - Scope-based authorization tests (4 tests)
|
||||
- `tests/server/test_mcp_oauth_jwt.py` - JWT OAuth integration tests
|
||||
- `tests/conftest.py` - Shared fixtures for JWT testing
|
||||
|
||||
### Consent Scenario Tests
|
||||
|
||||
Four test scenarios verify scope-based tool filtering with different consent levels:
|
||||
|
||||
#### 1. No Custom Scopes (0 tools)
|
||||
```bash
|
||||
uv run pytest tests/server/test_scope_authorization.py::test_jwt_with_no_custom_scopes_returns_zero_tools -v
|
||||
```
|
||||
|
||||
**Scenario:** JWT token with only OIDC defaults (`openid profile email`)
|
||||
**Expected:** 0 tools returned (all require `mcp:notes.read` or `mcp:notes.write`)
|
||||
**Verifies:** Security - users who decline custom scopes cannot access any MCP tools
|
||||
|
||||
#### 2. Read-Only Access (36 tools)
|
||||
```bash
|
||||
uv run pytest tests/server/test_scope_authorization.py::test_jwt_consent_scenarios_read_only -v
|
||||
```
|
||||
|
||||
**Scenario:** JWT token with `mcp:notes.read` only
|
||||
**Expected:** 36 read-only tools visible, write tools hidden
|
||||
**Verifies:** Read tools accessible, write tools filtered out
|
||||
|
||||
#### 3. Write-Only Access (54 tools)
|
||||
```bash
|
||||
uv run pytest tests/server/test_scope_authorization.py::test_jwt_consent_scenarios_write_only -v
|
||||
```
|
||||
|
||||
**Scenario:** JWT token with `mcp:notes.write` only
|
||||
**Expected:** 54 write tools visible, read tools hidden
|
||||
**Verifies:** Write tools accessible, read tools filtered out
|
||||
|
||||
#### 4. Full Access (90 tools)
|
||||
```bash
|
||||
uv run pytest tests/server/test_scope_authorization.py::test_jwt_consent_scenarios_full_access -v
|
||||
```
|
||||
|
||||
**Scenario:** JWT token with both `mcp:notes.read` and `mcp:notes.write`
|
||||
**Expected:** All 90 tools visible
|
||||
**Verifies:** Full access when user grants all custom scopes
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
**OAuth Client Fixtures:**
|
||||
- `read_only_oauth_client_credentials` - Client with `mcp:notes.read` only
|
||||
- `write_only_oauth_client_credentials` - Client with `mcp:notes.write` only
|
||||
- `full_access_oauth_client_credentials` - Client with both scopes
|
||||
- `no_custom_scopes_oauth_client_credentials` - Client with OIDC defaults only
|
||||
|
||||
**Token Fixtures:**
|
||||
- `playwright_oauth_token_read_only` - Obtains token with `mcp:notes.read`
|
||||
- `playwright_oauth_token_write_only` - Obtains token with `mcp:notes.write`
|
||||
- `playwright_oauth_token_full_access` - Obtains token with both scopes
|
||||
- `playwright_oauth_token_no_custom_scopes` - Obtains token with no custom scopes
|
||||
|
||||
**MCP Client Fixtures:**
|
||||
- `nc_mcp_oauth_client_read_only` - MCP session with read-only token
|
||||
- `nc_mcp_oauth_client_write_only` - MCP session with write-only token
|
||||
- `nc_mcp_oauth_client_full_access` - MCP session with full access token
|
||||
- `nc_mcp_oauth_client_no_custom_scopes` - MCP session with no custom scopes
|
||||
|
||||
### Running Tests
|
||||
|
||||
**All consent scenario tests:**
|
||||
```bash
|
||||
uv run pytest tests/server/test_scope_authorization.py -v
|
||||
```
|
||||
|
||||
**JWT OAuth integration tests:**
|
||||
```bash
|
||||
uv run pytest tests/server/test_mcp_oauth_jwt.py -v --browser firefox
|
||||
```
|
||||
|
||||
**With visible browser (debugging):**
|
||||
```bash
|
||||
uv run pytest tests/server/test_mcp_oauth_jwt.py -v --browser firefox --headed
|
||||
```
|
||||
|
||||
### Test Configuration
|
||||
|
||||
**Playwright Browser:**
|
||||
- Default: Chromium
|
||||
- Recommended for CI: Firefox (`--browser firefox`)
|
||||
- Debugging: Add `--headed` flag
|
||||
|
||||
**OAuth Flow:**
|
||||
- Uses automated Playwright browser automation
|
||||
- Completes OAuth consent flow programmatically
|
||||
- Creates separate OAuth client for each scenario
|
||||
- Each user gets unique access token
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: JWT Issuer Validation Failed
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
WARNING JWT issuer validation failed: Invalid issuer
|
||||
WARNING JWT verification failed, will try other methods
|
||||
✅ Extracted scopes from access token: {'openid', 'profile'}
|
||||
```
|
||||
|
||||
**Cause:** Token's `iss` claim doesn't match expected issuer URL. This often happens when:
|
||||
- Using `localhost` vs `127.0.0.1` inconsistently
|
||||
- MCP server uses internal URL but clients use public URL
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Option 1: Use consistent URLs
|
||||
export NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||
# Ensure all test fixtures also use localhost:8080
|
||||
|
||||
# Option 2: Check discovery document
|
||||
curl http://localhost:8080/.well-known/openid-configuration | jq .issuer
|
||||
# Use this exact issuer in NEXTCLOUD_PUBLIC_ISSUER_URL
|
||||
```
|
||||
|
||||
**Impact if not fixed:**
|
||||
- JWT validation falls back to userinfo endpoint
|
||||
- Scopes inferred from userinfo (only standard OIDC scopes, no custom scopes)
|
||||
- Result: 0 tools visible or incorrect tool filtering
|
||||
|
||||
### Issue: Scopes Not Present in JWT
|
||||
|
||||
**Symptom:** JWT token doesn't contain `scope` claim or contains empty string
|
||||
|
||||
**Cause:** Client's `allowed_scopes` is empty or not configured
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check client configuration
|
||||
docker compose exec app php occ oidc:list
|
||||
|
||||
# Look for allowed_scopes in output
|
||||
# If empty, recreate client with --allowed_scopes
|
||||
docker compose exec app php occ oidc:create \
|
||||
--token_type=jwt \
|
||||
--allowed_scopes="openid profile email mcp:notes.read mcp:notes.write" \
|
||||
"Client Name" \
|
||||
"http://callback/url"
|
||||
```
|
||||
|
||||
### Issue: All Tools Visible Despite Read-Only Token
|
||||
|
||||
**Symptom:** User with `mcp:notes.read` token can see all 90 tools including write tools
|
||||
|
||||
**Cause:** Server running in BasicAuth mode, not OAuth mode
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify OAuth mode is active
|
||||
docker compose logs mcp-oauth | grep "OAuth mode"
|
||||
|
||||
# Should see: "Running in OAuth mode"
|
||||
|
||||
# If not, check environment variables:
|
||||
docker compose exec mcp-oauth env | grep NEXTCLOUD_OIDC
|
||||
|
||||
# Ensure no NEXTCLOUD_USERNAME or NEXTCLOUD_PASSWORD set
|
||||
```
|
||||
|
||||
### Verifying DCR Scope Configuration
|
||||
|
||||
DCR **now properly sets `allowed_scopes`** when the `scope` parameter is provided during registration.
|
||||
|
||||
**To verify DCR scopes are working:**
|
||||
|
||||
```bash
|
||||
# Check the registered client's allowed_scopes via database
|
||||
docker compose exec db mariadb -u nextcloud -ppassword nextcloud \
|
||||
-e "SELECT name, allowed_scopes FROM oc_oauth2_clients WHERE name LIKE 'DCR-%' ORDER BY id DESC LIMIT 1;"
|
||||
|
||||
# Should show your requested scopes (e.g., "openid profile email mcp:notes.read mcp:notes.write")
|
||||
```
|
||||
|
||||
**If scopes are missing:**
|
||||
1. Ensure `NEXTCLOUD_OIDC_SCOPES` environment variable is set correctly
|
||||
2. Check MCP server startup logs for the scopes being requested
|
||||
3. Verify DCR is enabled in Nextcloud OIDC app settings
|
||||
4. Clear the SQLite database OAuth client entry and restart to force re-registration
|
||||
|
||||
### Issue: Token Type Case Sensitivity
|
||||
|
||||
**Symptom:** JWT tokens not generated even though `token_type=JWT` set
|
||||
|
||||
**Cause:** OIDC app checks `token_type === 'jwt'` (lowercase)
|
||||
|
||||
**Solution:** Always use lowercase:
|
||||
```bash
|
||||
# Correct
|
||||
export NEXTCLOUD_OIDC_TOKEN_TYPE=jwt
|
||||
|
||||
# Incorrect (will generate opaque tokens)
|
||||
export NEXTCLOUD_OIDC_TOKEN_TYPE=JWT
|
||||
```
|
||||
|
||||
### Issue: Missing WWW-Authenticate Header
|
||||
|
||||
**Symptom:** 403 error doesn't include `WWW-Authenticate` header
|
||||
|
||||
**Cause:** Server not in OAuth mode, or exception not being caught
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check server logs for OAuth mode
|
||||
docker compose logs mcp-oauth | grep "WWW-Authenticate scope challenges enabled"
|
||||
|
||||
# Should see this during startup
|
||||
|
||||
# Check exception handling
|
||||
docker compose logs mcp-oauth | grep "InsufficientScopeError"
|
||||
```
|
||||
|
||||
### Debugging Tools
|
||||
|
||||
**Check JWT contents:**
|
||||
```bash
|
||||
# Decode JWT (base64 decode the payload)
|
||||
echo "JWT_PAYLOAD_PART" | base64 -d | jq .
|
||||
```
|
||||
|
||||
**Check database scopes:**
|
||||
```bash
|
||||
# View access tokens with scopes
|
||||
docker compose exec db mariadb -u nextcloud -ppassword nextcloud \
|
||||
-e "SELECT id, client_id, user_id, scope FROM oc_oidc_access_tokens ORDER BY id DESC LIMIT 5;"
|
||||
|
||||
# View user consents
|
||||
docker compose exec db mariadb -u nextcloud -ppassword nextcloud \
|
||||
-e "SELECT user_id, client_id, scopes_granted FROM oc_oidc_user_consents;"
|
||||
```
|
||||
|
||||
**Check server logs:**
|
||||
```bash
|
||||
# Follow JWT verification logs
|
||||
docker compose logs -f mcp-oauth | grep -E "JWT|scope|tool"
|
||||
|
||||
# Check for issuer mismatches
|
||||
docker compose logs mcp-oauth | grep -i issuer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Deployment Checklist
|
||||
|
||||
✅ **Use JWT Tokens** - Enable `token_type=jwt` for better performance
|
||||
✅ **Configure Allowed Scopes** - Always set `allowed_scopes` on OAuth clients
|
||||
✅ **Use Pre-Configured Clients** - Avoid DCR limitation with manual client creation
|
||||
✅ **Consistent URLs** - Use same URL for `NEXTCLOUD_HOST` and `PUBLIC_ISSUER_URL`
|
||||
✅ **Secure Credentials** - Store client credentials securely (environment variables or secrets management)
|
||||
✅ **Monitor Token Size** - JWT tokens are 10-15x larger than opaque (not usually an issue)
|
||||
✅ **Enable Logging** - Configure appropriate log levels for JWT verification
|
||||
|
||||
### Production Configuration Example
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml (production)
|
||||
mcp-oauth:
|
||||
image: ghcr.io/yourusername/nextcloud-mcp-server:latest
|
||||
command: ["--transport", "streamable-http", "--oauth", "--port", "8001", "--oauth-token-type", "jwt"]
|
||||
environment:
|
||||
- NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||
- NEXTCLOUD_MCP_SERVER_URL=https://mcp.example.com
|
||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=https://nextcloud.example.com
|
||||
- NEXTCLOUD_OIDC_CLIENT_ID=${JWT_CLIENT_ID}
|
||||
- NEXTCLOUD_OIDC_CLIENT_SECRET=${JWT_CLIENT_SECRET}
|
||||
- NEXTCLOUD_OIDC_SCOPES=openid profile email mcp:notes.read mcp:notes.write
|
||||
ports:
|
||||
- "8001:8001"
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
|
||||
**Token Storage:**
|
||||
- Never commit credentials to version control
|
||||
- Use environment variables or secrets management
|
||||
- Rotate client secrets periodically
|
||||
|
||||
**Scope Configuration:**
|
||||
- Grant minimum necessary scopes to clients
|
||||
- Use read-only tokens for AI assistants that don't need write access
|
||||
- Review OAuth client list regularly
|
||||
|
||||
**Network Security:**
|
||||
- Use HTTPS in production
|
||||
- Ensure issuer URL matches public URL
|
||||
- Configure proper CORS headers
|
||||
|
||||
### Monitoring
|
||||
|
||||
**Key Metrics:**
|
||||
- JWT verification success/failure rate
|
||||
- Scope challenge frequency (indicates clients with insufficient scopes)
|
||||
- Token validation latency
|
||||
- Tool execution by scope (identify unused scopes)
|
||||
|
||||
**Log Patterns:**
|
||||
```bash
|
||||
# Success
|
||||
INFO JWT verified successfully for user: admin
|
||||
INFO ✅ Extracted scopes from access token: {'openid', 'profile', 'email', 'mcp:notes.read', 'mcp:notes.write'}
|
||||
|
||||
# Failures
|
||||
WARNING JWT issuer validation failed: Invalid issuer
|
||||
WARNING Missing required scopes: mcp:notes.write
|
||||
```
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **No Fine-Grained Scopes** - Only coarse `mcp:notes.read` and `mcp:notes.write` (not per-app scopes)
|
||||
2. **No Refresh Token Support** - Tokens must be reacquired when expired
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
**Potential Improvements:**
|
||||
- Per-app scopes (`nc:notes.read`, `nc:calendar.write`)
|
||||
- Resource-level filtering (apply to MCP resources, not just tools)
|
||||
- Automatic scope discovery from decorated tools
|
||||
- Admin UI for scope management
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Standards
|
||||
|
||||
- [RFC 9068: JWT Profile for OAuth 2.0 Access Tokens](https://www.rfc-editor.org/rfc/rfc9068.html)
|
||||
- [RFC 7519: JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519.html)
|
||||
- [RFC 7517: JSON Web Key (JWK)](https://www.rfc-editor.org/rfc/rfc7517.html)
|
||||
- [RFC 9728: OAuth 2.0 Protected Resource Metadata](https://www.rfc-editor.org/rfc/rfc9728.html)
|
||||
- [RFC 7662: OAuth 2.0 Token Introspection](https://www.rfc-editor.org/rfc/rfc7662.html)
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Complete OAuth configuration guide
|
||||
- [OAuth Architecture](oauth-architecture.md) - Detailed architecture documentation
|
||||
- [OAuth Troubleshooting](oauth-troubleshooting.md) - Common OAuth issues and solutions
|
||||
- [Authentication Guide](authentication.md) - BasicAuth vs OAuth comparison
|
||||
|
||||
### External Resources
|
||||
|
||||
- [Nextcloud OIDC App](https://github.com/H2CK/oidc) - OIDC identity provider for Nextcloud
|
||||
- [PyJWT Documentation](https://pyjwt.readthedocs.io/) - JWT library used for verification
|
||||
- [FastMCP Documentation](https://github.com/jlowin/fastmcp) - MCP server framework
|
||||
|
||||
---
|
||||
|
||||
**Implementation Date:** 2025-10-21 to 2025-10-23
|
||||
**Version:** 1.0.0
|
||||
**Status:** ✅ Production Ready
|
||||
@@ -0,0 +1,251 @@
|
||||
# Login Flow v2 (Multi-User Mode)
|
||||
|
||||
This is the recommended multi-user deployment mode for the Nextcloud MCP Server. It works with **stock Nextcloud 16+** (no upstream patches) and is the mode used by hosted offerings like [Astrolabe Cloud](https://astrolabecloud.com).
|
||||
|
||||
For the design rationale, see [ADR-022](ADR-022-deployment-mode-consolidation.md). For other deployment modes, see [Authentication](authentication.md).
|
||||
|
||||
## How It Works
|
||||
|
||||
Two authentication legs, each with a different mechanism:
|
||||
|
||||
```
|
||||
┌─────────────────┐ OAuth/OIDC ┌──────────────────┐ App password ┌─────────────────┐
|
||||
│ MCP Client │ ───────────────> │ MCP Server │ ────────────────> │ Nextcloud │
|
||||
│ (Claude, etc.) │ (mcp:* scopes) │ (OAuth issuer + │ (Basic Auth) │ (NC 16+) │
|
||||
│ │ │ app-pwd holder) │ │ │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
- **MCP client → MCP server**: OAuth 2.1 with PKCE. The MCP server is the authorization server (or proxies an external IdP). Tokens carry `mcp:*` scopes that gate which tools the user can call.
|
||||
- **MCP server → Nextcloud**: 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.
|
||||
|
||||
App passwords appear in **Settings → Security → Devices & Sessions** in Nextcloud and can be revoked by the user at any time.
|
||||
|
||||
### Why not forward OAuth bearer tokens to Nextcloud?
|
||||
|
||||
Earlier deployment modes forwarded the client's OAuth bearer token directly to Nextcloud APIs. That required upstream patches to `user_oidc` (Bearer-token validation on non-OCS endpoints) which were never merged. Nextcloud also doesn't enforce OAuth scopes on its app APIs even when Bearer tokens are accepted, so the security guarantees were weaker than they appeared. App passwords are the simplest mechanism that works on every supported Nextcloud version and surfaces user-revocable credentials in the standard UI.
|
||||
|
||||
Scope enforcement happens at the MCP server layer (defense-in-depth). See [Scope Enforcement](#scope-enforcement) below.
|
||||
|
||||
## Setup
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```bash
|
||||
# Nextcloud connection
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.example.com
|
||||
|
||||
# Enable Login Flow v2
|
||||
ENABLE_LOGIN_FLOW=true
|
||||
|
||||
# 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
|
||||
|
||||
# Public OAuth issuer URL (the URL clients will be redirected to for browser flows)
|
||||
NEXTCLOUD_MCP_SERVER_URL=https://mcp.example.com
|
||||
NEXTCLOUD_PUBLIC_ISSUER_URL=https://your.nextcloud.example.com # Public URL of Nextcloud
|
||||
```
|
||||
|
||||
### Generating an Encryption Key
|
||||
|
||||
App passwords are stored encrypted with Fernet. Generate a key once and reuse it:
|
||||
|
||||
```bash
|
||||
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
```
|
||||
|
||||
Lose the key and stored app passwords become unrecoverable — users will need to re-provision.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
The repo ships with a working reference under the `login-flow` profile:
|
||||
|
||||
```bash
|
||||
docker compose --profile login-flow up -d
|
||||
# Server listens on http://localhost:8004
|
||||
```
|
||||
|
||||
Excerpt from `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
mcp-login-flow:
|
||||
build: .
|
||||
command: ["--transport", "streamable-http", "--oauth", "--port", "8004"]
|
||||
ports:
|
||||
- 127.0.0.1:8004:8004
|
||||
environment:
|
||||
- NEXTCLOUD_HOST=http://app:80
|
||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004
|
||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||
- ENABLE_LOGIN_FLOW=true
|
||||
- TOKEN_ENCRYPTION_KEY=<your-fernet-key>
|
||||
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
volumes:
|
||||
- login-flow-data:/app/data
|
||||
- login-flow-oauth-storage:/app/.oauth
|
||||
```
|
||||
|
||||
The `--oauth` flag enables the OAuth/OIDC identity layer that Login Flow v2 builds on (user identity via OAuth session, Nextcloud access via app passwords).
|
||||
|
||||
## Per-User Provisioning Flow
|
||||
|
||||
Each user goes through provisioning **once**, the first time they connect. Subsequent requests reuse the stored app password.
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ MCP Client │ │ MCP Server │ │ Nextcloud │
|
||||
└──────┬──────┘ └────────┬─────────┘ └────────┬────────┘
|
||||
│ 1. OAuth PKCE │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ ← access token (mcp:* scopes) │ │
|
||||
│ │ │
|
||||
│ 2. MCP request │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ │ │
|
||||
│ 3. No stored app password → │ │
|
||||
│ elicit URL or 401 │ │
|
||||
│<─────────────────────────────────┤ │
|
||||
│ "Visit <login-url> to grant │ │
|
||||
│ access" │ │
|
||||
│ │ 4. POST /index.php/login/v2 │
|
||||
│ ├────────────────────────────────────>│
|
||||
│ │ ← {login_url, poll_endpoint, token}│
|
||||
│ │ │
|
||||
│ 5. User opens login_url in browser, authenticates, clicks "Grant" │
|
||||
│ ────────────────────────────────────────────────────────────────────> │
|
||||
│ │ │
|
||||
│ │ 6. Poll endpoint (background) │
|
||||
│ ├────────────────────────────────────>│
|
||||
│ │ ← {loginName, appPassword} │
|
||||
│ │ │
|
||||
│ │ 7. Encrypt + store in SQLite │
|
||||
│ │ │
|
||||
│ 8. Retry MCP request │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ │ 9. GET /apps/notes/... │
|
||||
│ ├────────────────────────────────────>│
|
||||
│ │ Authorization: Basic <app-pwd> │
|
||||
│ │ ← response │
|
||||
│ 10. ← result │ │
|
||||
```
|
||||
|
||||
### Provisioning Endpoints
|
||||
|
||||
The server exposes browser endpoints for management UIs (Astrolabe, custom dashboards):
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `GET /app/provision?redirect_uri=…` | Start Login Flow v2 and redirect to Nextcloud's grant page |
|
||||
| `GET /app/provision/status?id=…` | Check whether the background poll has completed |
|
||||
|
||||
Both require a valid OAuth bearer token in the `Authorization` header (the user's identity is taken from the token, not from a query parameter).
|
||||
|
||||
Implementation: [`nextcloud_mcp_server/auth/provision_routes.py`](../nextcloud_mcp_server/auth/provision_routes.py).
|
||||
|
||||
### Provisioning via MCP Tools (Elicitation)
|
||||
|
||||
For MCP clients, the same flow is exposed as tools (`nc_auth_provision_access`, `nc_auth_check_status`). Clients that support **URL elicitation** (MCP spec 2025-11-25) get a clickable link automatically; clients without that capability fall back to a copy-paste URL in an error message. See [ADR-022 §"MCP Elicitation for Login Flow v2"](ADR-022-deployment-mode-consolidation.md) for the full capability matrix.
|
||||
|
||||
## Scope Enforcement
|
||||
|
||||
Nextcloud's app passwords have **no native scope support** — they grant the user's full API access. The MCP server enforces scopes at the application layer.
|
||||
|
||||
### 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 |
|
||||
|
||||
> The historical `notes.read` / `notes.write` naming covers all Nextcloud apps, not just the Notes app — it's a project-wide read/write distinction.
|
||||
|
||||
Standard OIDC scopes (`openid`, `profile`, `email`) are also supported and have no effect on tool access.
|
||||
|
||||
### How Scopes Are Enforced
|
||||
|
||||
Each MCP tool is decorated with `@require_scopes(...)`:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
@require_scopes("mcp:notes.read")
|
||||
async def nc_notes_get_note(note_id: int, ctx: Context):
|
||||
...
|
||||
```
|
||||
|
||||
When a client calls `list_tools`, the server returns only tools the user has granted scopes for (dynamic tool filtering). When a client calls a tool whose scope is missing, the server returns:
|
||||
|
||||
```http
|
||||
HTTP/1.1 403 Forbidden
|
||||
WWW-Authenticate: Bearer error="insufficient_scope",
|
||||
scope="mcp:notes.write",
|
||||
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
|
||||
```
|
||||
|
||||
Clients can use this header to trigger **step-up authorization** — re-running the OAuth flow with additional scopes.
|
||||
|
||||
Implementation: [`nextcloud_mcp_server/auth/scope_authorization.py`](../nextcloud_mcp_server/auth/scope_authorization.py).
|
||||
|
||||
## OAuth Issuer Endpoints
|
||||
|
||||
When `--oauth` is enabled, the MCP server exposes OAuth 2.1 endpoints:
|
||||
|
||||
| Endpoint | RFC | Purpose |
|
||||
|----------|-----|---------|
|
||||
| `GET /.well-known/oauth-authorization-server` | RFC 8414 | Server metadata |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
Implementation: [`nextcloud_mcp_server/auth/oauth_routes.py`](../nextcloud_mcp_server/auth/oauth_routes.py), [`nextcloud_mcp_server/auth/client_registration.py`](../nextcloud_mcp_server/auth/client_registration.py).
|
||||
|
||||
PKCE with S256 is **mandatory** — required by the MCP specification and enforced at the authorization endpoint.
|
||||
|
||||
## Token Format
|
||||
|
||||
The MCP server can issue or accept either JWT or opaque access tokens depending on configuration.
|
||||
|
||||
| | JWT (recommended) | Opaque |
|
||||
|---|---|---|
|
||||
| Validation | Signature check via JWKS (local, fast) | Introspection HTTP call |
|
||||
| Scope claim | Embedded in `scope` claim | Returned by introspection endpoint |
|
||||
| Size | ~800-1200 chars | ~72 chars |
|
||||
| Standard | RFC 9068 | RFC 7662 |
|
||||
|
||||
JWTs are preferred for production because validation is local and stateless. Opaque tokens are useful when you need server-side revocation without JWT blocklist infrastructure.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "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.
|
||||
|
||||
### "Failed to start login flow" / 502 from `/app/provision`
|
||||
|
||||
The MCP server cannot reach Nextcloud at `NEXTCLOUD_HOST`. Verify network connectivity and that `NEXTCLOUD_HOST` uses an address reachable from the server (not the user's browser). For Docker Compose deployments, this is typically the internal service hostname (e.g. `http://app:80`).
|
||||
|
||||
### "Login URL points to localhost in browser"
|
||||
|
||||
`NEXTCLOUD_PUBLIC_ISSUER_URL` is missing or wrong. Set it to the public URL of Nextcloud as the user's browser sees it. The server rewrites the login URL's origin from the internal `NEXTCLOUD_HOST` to `NEXTCLOUD_PUBLIC_ISSUER_URL` before redirecting the browser.
|
||||
|
||||
### Stored app password rejected by Nextcloud (401)
|
||||
|
||||
The user revoked it from **Settings → Security → Devices & Sessions**. Delete the row from the storage DB (or call `nc_auth_provision_access` again) to trigger a fresh Login Flow.
|
||||
|
||||
### `cryptography.fernet.InvalidToken` on startup
|
||||
|
||||
`TOKEN_ENCRYPTION_KEY` changed since the DB was created — stored app passwords cannot be decrypted with a different key. Either restore the original key or wipe the DB and have users re-provision.
|
||||
|
||||
### Multiple worker processes
|
||||
|
||||
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.
|
||||
|
||||
## See Also
|
||||
|
||||
- [ADR-022: Deployment Mode Consolidation](ADR-022-deployment-mode-consolidation.md) — design rationale
|
||||
- [Authentication](authentication.md) — overview of all deployment modes
|
||||
- [Authentication Flows](auth-flows.md) — sequence diagrams per mode
|
||||
- [Configuration](configuration.md) — full environment variable reference
|
||||
- [Nextcloud Login Flow v2 spec](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2)
|
||||
@@ -1,752 +0,0 @@
|
||||
# OAuth Architecture
|
||||
|
||||
This document explains how OAuth2/OIDC authentication works in the Nextcloud MCP Server implementation.
|
||||
|
||||
## Overview
|
||||
|
||||
The Nextcloud MCP Server acts as an **OAuth 2.0 Resource Server**, protecting access to Nextcloud resources. It relies on Nextcloud's OIDC Identity Provider for user authentication and token validation.
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
The complete OAuth flow includes server startup (with DCR), client discovery (with PRM), authorization (with PKCE), and API access phases:
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Phase 0: MCP Server Startup & Client Registration (DCR - RFC 7591)
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
┌──────────────────┐ ┌─────────────────┐
|
||||
│ MCP Server │ │ Nextcloud │
|
||||
│ (Resource │ │ (OIDC Provider)│
|
||||
│ Server) │ │ │
|
||||
└────────┬─────────┘ └────────┬────────┘
|
||||
│ │
|
||||
│ 0a. OIDC Discovery │
|
||||
├────────────────────────────────────>│
|
||||
│ GET │
|
||||
| /.well-known/openid-configuration │
|
||||
│ │
|
||||
│ 0b. Discovery response │
|
||||
│<────────────────────────────────────┤
|
||||
│ {issuer, endpoints, PKCE methods} │
|
||||
│ │
|
||||
│ 0c. Register OAuth client (DCR) │
|
||||
├────────────────────────────────────>│
|
||||
│ POST /apps/oidc/register │
|
||||
│ {client_name, redirect_uris, │
|
||||
│ scopes, token_type} │
|
||||
│ │
|
||||
│ 0d. Client credentials │
|
||||
│<────────────────────────────────────┤
|
||||
│ {client_id, client_secret} │
|
||||
│ → Saved to SQLite database │
|
||||
│ │
|
||||
│ ✓ Server ready for connections │
|
||||
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Phase 1: Client Connection & Discovery (PRM - RFC 9728)
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ │ │ MCP Server │ │ Nextcloud │
|
||||
│ MCP Client │ │ (Resource │ │ Instance │
|
||||
│ (Claude) │ │ Server) │ │ │
|
||||
│ │ │ │ │ │
|
||||
└──────┬──────┘ └────────┬─────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
│ 1a. Connect to MCP │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ │ │
|
||||
│ 1b. Return auth settings │ │
|
||||
│<─────────────────────────────────┤ │
|
||||
│ {issuer_url, resource_url} │ │
|
||||
│ │ │
|
||||
│ 1c. PRM Discovery (RFC 9728) │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ GET /.well-known/oauth- │ │
|
||||
│ protected-resource/mcp │ │
|
||||
│ │ │
|
||||
│ 1d. PRM response (scopes!) │ │
|
||||
│<─────────────────────────────────┤ │
|
||||
│ {resource, scopes_supported, │ ← Dynamically discovered from │
|
||||
│ authorization_servers} │ @require_scopes decorators │
|
||||
│ │ │
|
||||
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Phase 2: OAuth Authorization Flow (PKCE - RFC 7636)
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
│ │ │
|
||||
│ 2a. Generate PKCE challenge │ │
|
||||
│ code_verifier = random(43-128) │ │
|
||||
│ code_challenge = SHA256(verif.) │ │
|
||||
│ │ │
|
||||
│ 2b. Authorization request │ │
|
||||
├──────────────────────────────────┼────────────────────────────────────>│
|
||||
│ /apps/oidc/authorize? │ │
|
||||
│ client_id=xxx │ │
|
||||
│ &code_challenge=abc... │ │
|
||||
│ &code_challenge_method=S256 │ │
|
||||
│ &scope=openid notes.read ... │ │
|
||||
│ │ │
|
||||
│ 2c. User consent page │ │
|
||||
│<─────────────────────────────────┼─────────────────────────────────────┤
|
||||
│ (Browser: Select scopes) │ │
|
||||
│ │ │
|
||||
│ 2d. User grants scopes │ │
|
||||
├──────────────────────────────────┼────────────────────────────────────>│
|
||||
│ │ │
|
||||
│ 2e. Authorization code redirect │ │
|
||||
│<─────────────────────────────────┼─────────────────────────────────────┤
|
||||
│ callback?code=xyz123 │ │
|
||||
│ │ │
|
||||
│ 2f. Exchange code for token │ │
|
||||
├──────────────────────────────────┼────────────────────────────────────>│
|
||||
│ POST /apps/oidc/token │ │
|
||||
│ {code, code_verifier, │ ← Validates PKCE challenge │
|
||||
│ client_id, client_secret} │ │
|
||||
│ │ │
|
||||
│ 2g. Access token (JWT/opaque) │ │
|
||||
│<─────────────────────────────────┼─────────────────────────────────────┤
|
||||
│ {access_token, token_type, │ │
|
||||
│ scope: "openid notes.read...") │ ← User's granted scopes │
|
||||
│ │ │
|
||||
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Phase 3: MCP Tool Access (Scope-based Authorization)
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
│ │ │
|
||||
│ 3a. list_tools request │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ Authorization: Bearer <token> │ │
|
||||
│ │ │
|
||||
│ │ 3b. Validate token │
|
||||
│ ├────────────────────────────────────>│
|
||||
│ │ GET /apps/oidc/userinfo │
|
||||
│ │ Authorization: Bearer <token> │
|
||||
│ │ │
|
||||
│ │ 3c. Token valid + scopes │
|
||||
│ │<────────────────────────────────────┤
|
||||
│ │ {sub, scopes, ...} │
|
||||
│ │ ← Cached for 1 hour │
|
||||
│ │ │
|
||||
│ 3d. Filtered tool list │ │
|
||||
│<─────────────────────────────────┤ ← Only tools matching user's │
|
||||
│ [tools matching token scopes] │ token scopes (via @require_scopes)
|
||||
│ │ │
|
||||
│ 3e. Call tool │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ nc_notes_get_note(note_id=1) │ ← @require_scopes("notes.read") │
|
||||
│ Authorization: Bearer <token> │ │
|
||||
│ │ │
|
||||
│ │ 3f. Scope check PASSED │
|
||||
│ │ ✓ Token has notes.read │
|
||||
│ │ │
|
||||
│ │ 3g. Nextcloud API call │
|
||||
│ ├────────────────────────────────────>│
|
||||
│ │ GET /apps/notes/api/v1/notes/1 │
|
||||
│ │ Authorization: Bearer <token> │
|
||||
│ │ ← user_oidc validates Bearer token │
|
||||
│ │ │
|
||||
│ │ 3h. API response │
|
||||
│ │<────────────────────────────────────┤
|
||||
│ │ {id: 1, title: "Note", ...} │
|
||||
│ │ │
|
||||
│ 3i. MCP tool response │ │
|
||||
│<─────────────────────────────────┤ │
|
||||
│ {note data} │ │
|
||||
│ │ │
|
||||
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Insufficient Scope Example (Step-Up Authorization)
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
│ 4a. Call write tool │ │
|
||||
├─────────────────────────────────>│ │
|
||||
│ nc_notes_create_note(...) │ ← @require_scopes("notes.write") │
|
||||
│ Authorization: Bearer <token> │ │
|
||||
│ │ │
|
||||
│ │ 4b. Scope check FAILED │
|
||||
│ │ ✗ Token only has notes.read │
|
||||
│ │ │
|
||||
│ 4c. 403 Insufficient Scope │ │
|
||||
│<─────────────────────────────────┤ │
|
||||
│ WWW-Authenticate: Bearer │ │
|
||||
│ error="insufficient_scope", │ │
|
||||
│ scope="notes.write", │ │
|
||||
│ resource_metadata="..." │ │
|
||||
│ │ │
|
||||
│ → Client can re-authorize with │ │
|
||||
│ additional scopes (Step-Up) │ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. MCP Client (e.g., Claude Desktop, Claude Code)
|
||||
|
||||
**Capabilities**:
|
||||
- Discovers OAuth configuration via MCP server
|
||||
- Queries PRM endpoint for supported scopes
|
||||
- Initiates OAuth flow with PKCE (Proof Key for Code Exchange)
|
||||
- Stores and sends access token with each request
|
||||
- Handles scope-based tool filtering
|
||||
- Supports step-up authorization (re-auth for additional scopes)
|
||||
|
||||
**Examples**: Claude Desktop, Claude Code, MCP Inspector, custom MCP clients
|
||||
|
||||
### 2. MCP Server (Resource Server - This Implementation)
|
||||
|
||||
**Role**: OAuth 2.0 Resource Server (RFC 6749)
|
||||
|
||||
**Responsibilities**:
|
||||
|
||||
#### Startup Phase
|
||||
- **OIDC Discovery**: Queries `/.well-known/openid-configuration` for OAuth endpoints
|
||||
- **PKCE Validation**: Verifies server advertises S256 code challenge method
|
||||
- **Dynamic Client Registration (DCR)**: Automatically registers OAuth client via `/apps/oidc/register` (RFC 7591)
|
||||
- Or loads pre-configured client credentials
|
||||
- Saves credentials to SQLite database
|
||||
- **Tool Registration**: Loads all MCP tools with their `@require_scopes` decorators
|
||||
|
||||
#### Client Connection Phase
|
||||
- **Auth Settings**: Returns OAuth issuer URL and resource URL
|
||||
- **PRM Endpoint**: Exposes `/.well-known/oauth-protected-resource/mcp` (RFC 9728)
|
||||
- Dynamically discovers scopes from all registered tools
|
||||
- Returns `scopes_supported` list based on `@require_scopes` decorators
|
||||
|
||||
#### Request Processing Phase
|
||||
- **Token Validation**: Validates Bearer tokens via Nextcloud userinfo endpoint
|
||||
- Supports both JWT and opaque tokens
|
||||
- Caches validation results (1-hour TTL)
|
||||
- Extracts user identity and granted scopes
|
||||
- **Scope Enforcement**:
|
||||
- Filters `list_tools` based on user's token scopes
|
||||
- Validates scopes before executing each tool
|
||||
- Returns 403 with `WWW-Authenticate` header for insufficient scopes
|
||||
- **Per-User Clients**: Creates authenticated `NextcloudClient` instance per user
|
||||
- Uses Bearer token for all Nextcloud API requests
|
||||
- User-specific permissions and audit trails
|
||||
|
||||
**Key Files**:
|
||||
- [`app.py`](../nextcloud_mcp_server/app.py) - OAuth mode, DCR, PRM endpoint
|
||||
- [`auth/token_verifier.py`](../nextcloud_mcp_server/auth/token_verifier.py) - Token validation (userinfo + introspection + JWT)
|
||||
- [`auth/context_helper.py`](../nextcloud_mcp_server/auth/context_helper.py) - Per-user client creation
|
||||
- [`auth/scope_authorization.py`](../nextcloud_mcp_server/auth/scope_authorization.py) - `@require_scopes` decorator, scope discovery
|
||||
- [`auth/client_registration.py`](../nextcloud_mcp_server/auth/client_registration.py) - DCR implementation (RFC 7591)
|
||||
|
||||
### 3. Nextcloud OIDC Apps
|
||||
|
||||
#### a) `oidc` - OIDC Identity Provider
|
||||
|
||||
**Role**: OAuth 2.0 Authorization Server + OIDC Provider
|
||||
|
||||
**Location**: Nextcloud app (`apps/oidc`)
|
||||
|
||||
**Endpoints**:
|
||||
- `/.well-known/openid-configuration` - OIDC Discovery (RFC 8414)
|
||||
- `/apps/oidc/authorize` - Authorization endpoint (OAuth 2.0 + PKCE)
|
||||
- `/apps/oidc/token` - Token endpoint (issues JWT or opaque tokens)
|
||||
- `/apps/oidc/userinfo` - UserInfo endpoint (OIDC Core, used for token validation)
|
||||
- `/apps/oidc/jwks` - JSON Web Key Set (for JWT signature verification)
|
||||
- `/apps/oidc/register` - Dynamic Client Registration endpoint (RFC 7591)
|
||||
- `/apps/oidc/introspect` - Token Introspection endpoint (RFC 7662, optional)
|
||||
|
||||
**Token Types**:
|
||||
- **JWT tokens**: Self-contained tokens with embedded scopes, validated via JWKS or userinfo
|
||||
- **Opaque tokens**: Random strings, validated via userinfo or introspection endpoint
|
||||
|
||||
**Configuration**:
|
||||
```bash
|
||||
# Enable dynamic client registration (recommended for development)
|
||||
# Nextcloud Admin → Settings → OIDC → "Allow dynamic client registration"
|
||||
|
||||
# Enable token introspection (optional, for opaque token validation)
|
||||
# Nextcloud Admin → Settings → OIDC → "Enable token introspection"
|
||||
```
|
||||
|
||||
#### b) `user_oidc` - OpenID Connect User Backend
|
||||
|
||||
**Role**: Bearer token validation middleware for Nextcloud APIs
|
||||
|
||||
**Location**: Nextcloud app (`apps/user_oidc`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Intercepts Nextcloud API requests with `Authorization: Bearer` header
|
||||
- Validates tokens against OIDC provider (`oidc` app)
|
||||
- Creates authenticated user sessions
|
||||
- Enforces user-specific permissions on API requests
|
||||
|
||||
**Configuration**:
|
||||
```bash
|
||||
# Enable Bearer token validation (required for OAuth mode)
|
||||
php occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `user_oidc` app requires a patch to properly support Bearer token authentication for non-OCS endpoints (like Notes API, Calendar API). See [Upstream Status](oauth-upstream-status.md) for patch details and PR status.
|
||||
|
||||
### 4. Nextcloud Instance
|
||||
|
||||
**Role**: Resource Owner + API Provider
|
||||
|
||||
**APIs Exposed**:
|
||||
- **Notes API**: `/apps/notes/api/v1/` - Note CRUD operations
|
||||
- **Calendar (CalDAV)**: `/remote.php/dav/calendars/` - Events and todos
|
||||
- **Contacts (CardDAV)**: `/remote.php/dav/addressbooks/` - Contact management
|
||||
- **Cookbook API**: `/apps/cookbook/api/v1/` - Recipe management
|
||||
- **Deck API**: `/apps/deck/api/v1.0/` - Kanban boards
|
||||
- **Tables API**: `/apps/tables/api/2/` - Table row operations
|
||||
- **WebDAV (Files)**: `/remote.php/dav/files/` - File operations
|
||||
- **Sharing API**: `/ocs/v2.php/apps/files_sharing/api/v1/` - Share management
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
The OAuth flow consists of four distinct phases (see diagram above for visual representation):
|
||||
|
||||
### Phase 0: MCP Server Startup (One-time Setup)
|
||||
|
||||
**Happens**: On MCP server first startup
|
||||
|
||||
**Steps**:
|
||||
1. **OIDC Discovery** (`GET /.well-known/openid-configuration`)
|
||||
- MCP server queries Nextcloud for OAuth endpoints
|
||||
- Validates PKCE support (requires `S256` code challenge method)
|
||||
- Extracts endpoints: authorize, token, userinfo, jwks, register
|
||||
|
||||
2. **Dynamic Client Registration** (`POST /apps/oidc/register`)
|
||||
- If no pre-configured client credentials exist
|
||||
- MCP server registers itself as OAuth client (RFC 7591)
|
||||
- Provides: client name, redirect URIs, requested scopes, token type
|
||||
- Receives: `client_id`, `client_secret`
|
||||
- Saves credentials to SQLite database
|
||||
|
||||
3. **Tool Registration**
|
||||
- All MCP tools loaded with their `@require_scopes` decorators
|
||||
- Scope metadata stored for later discovery
|
||||
|
||||
**Result**: MCP server ready to accept client connections
|
||||
|
||||
### Phase 1: Client Discovery (Per MCP Client Connection)
|
||||
|
||||
**Happens**: When MCP client first connects
|
||||
|
||||
**Steps**:
|
||||
1. **MCP Connection**
|
||||
- Client connects to MCP server
|
||||
- Server returns OAuth auth settings (issuer URL, resource URL)
|
||||
|
||||
2. **PRM Discovery** (`GET /.well-known/oauth-protected-resource/mcp`)
|
||||
- Client queries Protected Resource Metadata endpoint (RFC 9728)
|
||||
- Server **dynamically discovers** scopes from all registered tools
|
||||
- Returns: resource URL, `scopes_supported` list, authorization servers
|
||||
- Client now knows which scopes are available
|
||||
|
||||
**Result**: Client knows OAuth configuration and available scopes
|
||||
|
||||
### Phase 2: OAuth Authorization (PKCE Flow - RFC 7636)
|
||||
|
||||
**Happens**: User authorizes access
|
||||
|
||||
**Steps**:
|
||||
1. **PKCE Challenge Generation** (Client-side)
|
||||
- Generate `code_verifier`: random 43-128 character string
|
||||
- Calculate `code_challenge`: `BASE64URL(SHA256(code_verifier))`
|
||||
|
||||
2. **Authorization Request** (`GET /apps/oidc/authorize`)
|
||||
- Client redirects user to Nextcloud consent page
|
||||
- Parameters:
|
||||
- `client_id`: OAuth client ID
|
||||
- `code_challenge`: SHA256 hash of verifier
|
||||
- `code_challenge_method`: `S256`
|
||||
- `scope`: Requested scopes (e.g., `openid notes.read notes.write`)
|
||||
- `redirect_uri`: MCP server callback URL
|
||||
|
||||
3. **User Consent**
|
||||
- User authenticates to Nextcloud (if not already logged in)
|
||||
- User reviews and approves/denies requested scopes
|
||||
- Can select subset of requested scopes
|
||||
|
||||
4. **Authorization Code**
|
||||
- Nextcloud redirects to `callback?code=xyz123`
|
||||
- Code is bound to PKCE challenge
|
||||
|
||||
5. **Token Exchange** (`POST /apps/oidc/token`)
|
||||
- Client sends:
|
||||
- Authorization `code`
|
||||
- `code_verifier` (proves possession of original challenge)
|
||||
- `client_id` and `client_secret`
|
||||
- Nextcloud validates PKCE challenge: `SHA256(code_verifier) == code_challenge`
|
||||
- Nextcloud issues access token
|
||||
|
||||
6. **Access Token Response**
|
||||
- Token type: JWT or opaque (configurable)
|
||||
- Contains user's **granted scopes** (may be subset of requested)
|
||||
- Client stores token for subsequent requests
|
||||
|
||||
**Result**: Client has valid access token with granted scopes
|
||||
|
||||
### Phase 3: MCP Tool Access (Scope-Based Authorization)
|
||||
|
||||
**Happens**: Every MCP tool invocation
|
||||
|
||||
**Steps**:
|
||||
|
||||
#### Tool Listing (`list_tools`)
|
||||
1. **List Tools Request**
|
||||
- Client sends `list_tools` with `Authorization: Bearer <token>`
|
||||
|
||||
2. **Token Validation**
|
||||
- MCP server calls `/apps/oidc/userinfo` with Bearer token
|
||||
- Nextcloud returns user info including **granted scopes**
|
||||
- Result cached for 1 hour
|
||||
|
||||
3. **Dynamic Tool Filtering**
|
||||
- Server compares token scopes with each tool's `@require_scopes`
|
||||
- Only returns tools where user has all required scopes
|
||||
- Example: Token with `notes.read` sees 4 read tools, not 3 write tools
|
||||
|
||||
4. **Filtered Tool List**
|
||||
- Client receives only tools they can use
|
||||
|
||||
#### Tool Execution (e.g., `nc_notes_get_note`)
|
||||
1. **Tool Call**
|
||||
- Client invokes tool with `Authorization: Bearer <token>`
|
||||
|
||||
2. **Scope Validation**
|
||||
- `@require_scopes` decorator extracts token scopes
|
||||
- Verifies token contains required scope (e.g., `notes.read`)
|
||||
- If missing → 403 with `WWW-Authenticate` header (step-up auth)
|
||||
- If present → continues execution
|
||||
|
||||
3. **Nextcloud API Call**
|
||||
- MCP server creates `NextcloudClient` with Bearer token
|
||||
- Calls Nextcloud API (e.g., `GET /apps/notes/api/v1/notes/1`)
|
||||
- `user_oidc` app validates Bearer token again
|
||||
- Request executes as authenticated user
|
||||
|
||||
4. **Response**
|
||||
- Nextcloud returns data
|
||||
- MCP server formats response
|
||||
- Returns to client
|
||||
|
||||
**Result**: User can only access tools and data they have permissions for
|
||||
|
||||
### Phase 4: Insufficient Scope Handling (Step-Up Authorization)
|
||||
|
||||
**Happens**: When user lacks required scopes
|
||||
|
||||
**Steps**:
|
||||
1. **Tool Call with Insufficient Scopes**
|
||||
- User calls `nc_notes_create_note` (requires `notes.write`)
|
||||
- But token only has `notes.read`
|
||||
|
||||
2. **Scope Validation Fails**
|
||||
- `@require_scopes("notes.write")` decorator checks token
|
||||
- Finds `notes.write` missing
|
||||
|
||||
3. **403 Response with Challenge**
|
||||
- Returns `403 Forbidden`
|
||||
- Includes `WWW-Authenticate` header:
|
||||
```
|
||||
Bearer error="insufficient_scope",
|
||||
scope="notes.write",
|
||||
resource_metadata="http://localhost:8000/.well-known/oauth-protected-resource/mcp"
|
||||
```
|
||||
|
||||
4. **Client Re-Authorization** (Optional)
|
||||
- Client can initiate new OAuth flow requesting additional scopes
|
||||
- User re-consents with expanded permissions
|
||||
- New token includes both `notes.read` and `notes.write`
|
||||
|
||||
**Result**: User can dynamically upgrade permissions without full re-authentication
|
||||
|
||||
## Token Validation
|
||||
|
||||
The MCP server validates tokens using the **userinfo endpoint approach**:
|
||||
|
||||
### Why Userinfo (vs JWT Validation)?
|
||||
|
||||
**Advantages**:
|
||||
- Works with both JWT and opaque tokens
|
||||
- No need to manage JWKS rotation
|
||||
- Always up-to-date (respects token revocation)
|
||||
- Simpler implementation
|
||||
|
||||
**Caching Strategy**:
|
||||
- Validated tokens cached for 1 hour (configurable)
|
||||
- Cache keyed by token string
|
||||
- Expired tokens re-validated automatically
|
||||
|
||||
**Implementation**: See [`NextcloudTokenVerifier`](../nextcloud_mcp_server/auth/token_verifier.py)
|
||||
|
||||
## PKCE Requirement
|
||||
|
||||
The MCP server **requires** PKCE with S256 code challenge method:
|
||||
|
||||
1. Server validates OIDC discovery advertises PKCE support
|
||||
2. Checks for `code_challenge_methods_supported` field
|
||||
3. Verifies `S256` is included in supported methods
|
||||
4. Logs error if PKCE not properly advertised
|
||||
|
||||
**Why PKCE?**:
|
||||
- Required by MCP specification
|
||||
- Protects against authorization code interception
|
||||
- Essential for public clients (desktop apps, CLI tools)
|
||||
|
||||
**Implementation**: See [`validate_pkce_support()`](../nextcloud_mcp_server/app.py#L31-L93)
|
||||
|
||||
## Client Registration
|
||||
|
||||
The MCP server supports two client registration modes:
|
||||
|
||||
### Automatic Registration (Dynamic Client Registration)
|
||||
|
||||
```bash
|
||||
# No client credentials needed
|
||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
1. Server checks `/.well-known/openid-configuration` for `registration_endpoint`
|
||||
2. Calls `/apps/oidc/register` to register a client on first startup
|
||||
3. Saves credentials to SQLite database
|
||||
4. Reuses these credentials on subsequent startups
|
||||
5. Re-registers only if credentials are missing or expired
|
||||
|
||||
**Best for**: Development, testing, quick deployments
|
||||
|
||||
### Pre-configured Client
|
||||
|
||||
```bash
|
||||
# Manual client registration via CLI
|
||||
php occ oidc:create --name="MCP Server" --type=confidential --redirect-uri="http://localhost:8000/oauth/callback"
|
||||
|
||||
# Configure MCP server
|
||||
NEXTCLOUD_HOST=https://nextcloud.example.com
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=abc123
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=xyz789
|
||||
```
|
||||
|
||||
**Best for**: Production, long-running deployments
|
||||
|
||||
## Per-User Client Instances
|
||||
|
||||
Each authenticated user gets their own `NextcloudClient` instance:
|
||||
|
||||
```python
|
||||
# From MCP context (contains validated token)
|
||||
client = get_client_from_context(ctx)
|
||||
|
||||
# Creates NextcloudClient with:
|
||||
# - username: from token's 'sub' or 'preferred_username' claim
|
||||
# - auth: BearerAuth(token)
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- User-specific permissions
|
||||
- Audit trail (actions appear from correct user)
|
||||
- No shared credentials
|
||||
- Multi-user support
|
||||
|
||||
**Implementation**: See [`get_client_from_context()`](../nextcloud_mcp_server/auth/context_helper.py)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Storage
|
||||
- MCP client stores access token
|
||||
- MCP server does NOT store tokens (validates per-request)
|
||||
- Token validation results cached in-memory only
|
||||
|
||||
### PKCE Protection
|
||||
- Server validates PKCE is advertised
|
||||
- Client MUST use PKCE with S256
|
||||
- Protects against authorization code interception
|
||||
|
||||
### Scopes
|
||||
- Base required scopes: `openid`, `profile`, `email`
|
||||
- App-specific scopes control access to individual Nextcloud apps
|
||||
- See [OAuth Scopes](#oauth-scopes) section for complete scope reference
|
||||
|
||||
### Token Validation
|
||||
- Every MCP request validates Bearer token
|
||||
- Cached for performance (1-hour default)
|
||||
- Calls userinfo endpoint for validation
|
||||
|
||||
## OAuth Scopes
|
||||
|
||||
The Nextcloud MCP Server implements fine-grained OAuth scopes for each Nextcloud app integration. Scopes control which tools are visible and accessible to users based on their granted permissions.
|
||||
|
||||
### Scope-Based Access Control
|
||||
|
||||
When using OAuth authentication:
|
||||
1. **Dynamic Discovery**: The server automatically discovers all required scopes from `@require_scopes` decorators on MCP tools
|
||||
2. **Tool Filtering**: Tools are dynamically filtered based on the user's token scopes - users only see tools they have permission to use
|
||||
3. **Per-Tool Enforcement**: Each tool validates required scopes before execution, returning a 403 error if insufficient scopes are present
|
||||
|
||||
### Supported Scopes
|
||||
|
||||
The server supports the following OAuth scopes, organized by Nextcloud app:
|
||||
|
||||
#### Base OIDC Scopes
|
||||
- `openid` - OpenID Connect authentication (required)
|
||||
- `profile` - Access to user profile information (required)
|
||||
- `email` - Access to user email address (required)
|
||||
|
||||
#### Notes App
|
||||
- `notes.read` - Read notes, search notes, get note attachments
|
||||
- `notes.write` - Create, update, append to, and delete notes
|
||||
|
||||
#### Calendar App
|
||||
- `calendar.read` - List calendars, read events, search events
|
||||
- `calendar.write` - Create, update, and delete calendars and events
|
||||
|
||||
#### Calendar Tasks (VTODO)
|
||||
- `todo.read` - List and read CalDAV tasks
|
||||
- `todo.write` - Create, update, and delete CalDAV tasks
|
||||
|
||||
#### Contacts App
|
||||
- `contacts.read` - List address books and read contacts (CardDAV)
|
||||
- `contacts.write` - Create, update, and delete address books and contacts
|
||||
|
||||
#### Cookbook App
|
||||
- `cookbook.read` - Read recipes, search recipes
|
||||
- `cookbook.write` - Create, update, and delete recipes
|
||||
|
||||
#### Deck App
|
||||
- `deck.read` - List boards, stacks, cards, and labels
|
||||
- `deck.write` - Create, update, and delete boards, stacks, cards, and labels
|
||||
|
||||
#### Tables App
|
||||
- `tables.read` - List tables and read rows
|
||||
- `tables.write` - Create, update, and delete rows in tables
|
||||
|
||||
#### Files (WebDAV)
|
||||
- `files.read` - List files, read file contents, search files
|
||||
- `files.write` - Upload, update, move, copy, and delete files
|
||||
|
||||
#### Sharing
|
||||
- `sharing.read` - List shares and read share information
|
||||
- `sharing.write` - Create, update, and delete shares
|
||||
|
||||
#### Semantic Search (Multi-App Vector Database)
|
||||
- `semantic.read` - Query vector database, perform semantic search across all indexed Nextcloud apps (notes, calendar, deck, files, contacts)
|
||||
- `semantic.write` - Enable/disable background vector synchronization, manage indexing settings
|
||||
|
||||
> **Note**: Semantic search scopes provide access to the vector database that indexes content across **all** Nextcloud apps. Unlike app-specific scopes (e.g., `notes.read`), semantic scopes grant cross-app search capabilities powered by background vector synchronization (ADR-007).
|
||||
|
||||
### Scope Discovery
|
||||
|
||||
The MCP server provides scope discovery through two mechanisms:
|
||||
|
||||
#### 1. Protected Resource Metadata (PRM) Endpoint
|
||||
```bash
|
||||
# Query the PRM endpoint
|
||||
curl http://localhost:8000/.well-known/oauth-protected-resource/mcp
|
||||
|
||||
# Response includes dynamically discovered scopes
|
||||
{
|
||||
"resource": "http://localhost:8000/mcp",
|
||||
"scopes_supported": ["openid", "profile", "email", "notes.read", ...],
|
||||
"authorization_servers": ["https://nextcloud.example.com"],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"resource_signing_alg_values_supported": ["RS256"]
|
||||
}
|
||||
```
|
||||
|
||||
The `scopes_supported` field is **dynamically generated** from all registered MCP tools, ensuring it always reflects the actual available scopes.
|
||||
|
||||
#### 2. Scope Enforcement via Decorators
|
||||
|
||||
Tools are decorated with `@require_scopes()` to declare their required permissions:
|
||||
|
||||
```python
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
|
||||
@mcp.tool()
|
||||
@require_scopes("notes.read")
|
||||
async def nc_notes_get_note(ctx: Context, note_id: int):
|
||||
"""Get a specific note by ID"""
|
||||
# Implementation
|
||||
```
|
||||
|
||||
### Client Registration Scopes
|
||||
|
||||
During OAuth client registration (dynamic or manual), clients request a set of scopes that define the **maximum allowed** scopes for that client. The actual per-tool enforcement is handled separately via decorators.
|
||||
|
||||
**Environment Variable**:
|
||||
```bash
|
||||
NEXTCLOUD_OIDC_SCOPES="openid profile email notes.read notes.write calendar.read calendar.write ..."
|
||||
```
|
||||
|
||||
**Default**: All supported scopes (recommended for development)
|
||||
|
||||
> **Note**: Client registration scopes define the maximum permissions. The MCP server's PRM endpoint dynamically advertises the actual supported scopes based on registered tools.
|
||||
|
||||
### Step-Up Authorization
|
||||
|
||||
The server supports OAuth step-up authorization (RFC 8693). If a user attempts to use a tool requiring scopes they don't have:
|
||||
|
||||
1. Tool returns `403 Forbidden` with `InsufficientScopeError`
|
||||
2. Response includes `WWW-Authenticate` header listing missing scopes:
|
||||
```
|
||||
WWW-Authenticate: Bearer error="insufficient_scope", scope="notes.write", resource_metadata="..."
|
||||
```
|
||||
3. Client can re-authorize with additional scopes
|
||||
|
||||
### Scope Validation
|
||||
|
||||
All scope enforcement happens at two levels:
|
||||
|
||||
1. **Tool Visibility**: During `list_tools` requests, only tools matching the user's token scopes are returned
|
||||
2. **Execution Time**: When calling a tool, the `@require_scopes` decorator validates the token has necessary scopes
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
# User token has: ["openid", "profile", "email", "notes.read"]
|
||||
# They will see: 4 read-only notes tools
|
||||
# They will NOT see: 3 write notes tools (notes.write required)
|
||||
# Attempting to call a write tool returns 403 Forbidden
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
See [Configuration Guide](configuration.md) for all OAuth environment variables:
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `NEXTCLOUD_HOST` | Nextcloud instance URL |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_ID` | Pre-configured client ID (optional) |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_SECRET` | Pre-configured client secret (optional) |
|
||||
| `NEXTCLOUD_MCP_SERVER_URL` | MCP server URL for OAuth callbacks |
|
||||
|
||||
## Testing
|
||||
|
||||
The integration test suite includes comprehensive OAuth testing:
|
||||
|
||||
- **Automated tests** (Playwright): [`tests/client/test_oauth_playwright.py`](../tests/client/test_oauth_playwright.py)
|
||||
- **Fixtures**: [`tests/conftest.py`](../tests/conftest.py)
|
||||
|
||||
Run OAuth tests:
|
||||
```bash
|
||||
# Start OAuth-enabled MCP server
|
||||
docker-compose up --build -d mcp-oauth
|
||||
|
||||
# Run automated tests
|
||||
uv run pytest tests/client/test_oauth_playwright.py --browser firefox -v
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Configuration steps
|
||||
- [OAuth Quick Start](quickstart-oauth.md) - Get started quickly
|
||||
- [Upstream Status](oauth-upstream-status.md) - Required upstream patches
|
||||
- [OAuth Troubleshooting](oauth-troubleshooting.md) - Common issues
|
||||
- [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) - OAuth 2.0 Authorization Framework
|
||||
- [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636) - PKCE
|
||||
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
|
||||
@@ -1,541 +0,0 @@
|
||||
# OAuth Setup Guide
|
||||
|
||||
This guide walks you through setting up OAuth2/OIDC authentication for the Nextcloud MCP server in production.
|
||||
|
||||
> **Quick Start?** If you want a 5-minute setup for development, see [OAuth Quick Start](quickstart-oauth.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Step 1: Install Nextcloud Apps](#step-1-install-nextcloud-apps)
|
||||
- [Step 2: Configure OIDC Apps](#step-2-configure-oidc-apps)
|
||||
- [Step 3: Choose Deployment Mode](#step-3-choose-deployment-mode)
|
||||
- [Step 4: Configure MCP Server](#step-4-configure-mcp-server)
|
||||
- [Step 5: Start and Verify](#step-5-start-and-verify)
|
||||
- [Testing Authentication](#testing-authentication)
|
||||
- [Production Recommendations](#production-recommendations)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before beginning, ensure you have:
|
||||
|
||||
- **Nextcloud instance** with administrator access
|
||||
- **Nextcloud version** 28 or later
|
||||
- **SSH/CLI access** to Nextcloud server (for `occ` commands)
|
||||
- **Python 3.11+** installed on MCP server host
|
||||
- **MCP server installed** (see [Installation Guide](installation.md))
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The OAuth implementation uses the following components:
|
||||
|
||||
```
|
||||
MCP Client ←→ MCP Server (Resource Server) ←→ Nextcloud (Authorization Server + APIs)
|
||||
OAuth Flow Bearer Token Auth
|
||||
```
|
||||
|
||||
**Key Roles**:
|
||||
- **MCP Server**: OAuth Resource Server (validates tokens, provides MCP tools)
|
||||
- **Nextcloud `oidc` app**: OAuth Authorization Server (issues tokens)
|
||||
- **Nextcloud `user_oidc` app**: Token validation middleware
|
||||
|
||||
For detailed architecture, see [OAuth Architecture](oauth-architecture.md).
|
||||
|
||||
## Step 1: Install Nextcloud Apps
|
||||
|
||||
OAuth authentication requires **two Nextcloud apps** to work together.
|
||||
|
||||
### Required Apps
|
||||
|
||||
#### 1. `oidc` - OIDC Identity Provider
|
||||
|
||||
**Purpose**: Makes Nextcloud an OAuth2/OIDC authorization server
|
||||
|
||||
**Installation**:
|
||||
1. Open Nextcloud as administrator
|
||||
2. Navigate to **Apps** → **Security**
|
||||
3. Find **"OIDC"** (full name: "OIDC Identity Provider")
|
||||
4. Click **Enable** or **Download and enable**
|
||||
|
||||
**Provides**:
|
||||
- OAuth2 authorization endpoint
|
||||
- Token endpoint
|
||||
- User info endpoint
|
||||
- JWKS endpoint
|
||||
- Dynamic client registration endpoint (optional)
|
||||
|
||||
#### 2. `user_oidc` - OpenID Connect User Backend
|
||||
|
||||
**Purpose**: Authenticates users and validates Bearer tokens
|
||||
|
||||
**Installation**:
|
||||
1. In **Apps** → **Security**
|
||||
2. Find **"OpenID Connect user backend"** (app ID: `user_oidc`)
|
||||
3. Click **Enable** or **Download and enable**
|
||||
|
||||
**Provides**:
|
||||
- Bearer token validation against OIDC provider
|
||||
- User authentication via OIDC
|
||||
- Session management for authenticated users
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Upstream Patch Required**: The `user_oidc` app needs a patch for Bearer token support with app-specific APIs (Notes, Calendar, etc.). The patch is pending upstream review.
|
||||
>
|
||||
> **Status**: See [Upstream Status](oauth-upstream-status.md) for current PR status and workarounds.
|
||||
>
|
||||
> **Impact**: OCS APIs work without patch, but app-specific APIs require the patch.
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
# Check both apps are installed and enabled
|
||||
php occ app:list | grep -E "oidc|user_oidc"
|
||||
|
||||
# Expected output:
|
||||
# - oidc: enabled
|
||||
# - user_oidc: enabled
|
||||
```
|
||||
|
||||
## Step 2: Configure OIDC Apps
|
||||
|
||||
### Configure `oidc` App (Identity Provider)
|
||||
|
||||
#### Option A: Dynamic Client Registration (Development)
|
||||
|
||||
**Best for**: Development, testing, auto-registration
|
||||
|
||||
1. Navigate to **Settings** → **OIDC** (Administration settings)
|
||||
2. Enable **"Allow dynamic client registration"**
|
||||
3. (Optional) Configure client expiration:
|
||||
```bash
|
||||
# Default: 3600 seconds (1 hour)
|
||||
php occ config:app:set oidc expire_time --value "86400" # 24 hours
|
||||
```
|
||||
|
||||
#### Option B: Pre-configured Clients (Production)
|
||||
|
||||
**Best for**: Production, long-running deployments
|
||||
|
||||
Skip the dynamic registration setting. You'll manually register clients via CLI in Step 3.
|
||||
|
||||
### Configure `user_oidc` App (Token Validation)
|
||||
|
||||
**Required**: Enable Bearer token validation:
|
||||
|
||||
```bash
|
||||
# SSH into Nextcloud server
|
||||
php occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean
|
||||
```
|
||||
|
||||
This tells `user_oidc` to validate Bearer tokens against Nextcloud's OIDC Identity Provider.
|
||||
|
||||
### Verify OIDC Discovery
|
||||
|
||||
Test that OIDC discovery endpoint is accessible:
|
||||
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration | jq
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"issuer": "https://your.nextcloud.instance.com",
|
||||
"authorization_endpoint": "https://your.nextcloud.instance.com/apps/oidc/authorize",
|
||||
"token_endpoint": "https://your.nextcloud.instance.com/apps/oidc/token",
|
||||
"userinfo_endpoint": "https://your.nextcloud.instance.com/apps/oidc/userinfo",
|
||||
"jwks_uri": "https://your.nextcloud.instance.com/apps/oidc/jwks",
|
||||
"registration_endpoint": "https://your.nextcloud.instance.com/apps/oidc/register",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### PKCE Support
|
||||
|
||||
The MCP server **requires PKCE** (Proof Key for Code Exchange) with S256 code challenge method.
|
||||
|
||||
**Validation**: The MCP server automatically validates PKCE support at startup by checking the discovery response for `code_challenge_methods_supported`.
|
||||
|
||||
**Note**: If PKCE is not advertised in discovery metadata, the server logs a warning but continues (PKCE still works, it's just not advertised). See [Upstream Status](oauth-upstream-status.md) for tracking.
|
||||
|
||||
## Step 3: Choose Deployment Mode
|
||||
|
||||
You have two options for managing OAuth clients:
|
||||
|
||||
### Mode A: Automatic Registration (Dynamic Client Registration)
|
||||
|
||||
**Best for**: Development, testing, quick deployments
|
||||
|
||||
**How it works**:
|
||||
- MCP server automatically registers an OAuth client on first startup
|
||||
- Uses Nextcloud's dynamic client registration endpoint
|
||||
- Saves credentials to SQLite database
|
||||
- Reuses stored credentials on subsequent restarts
|
||||
- Re-registers automatically if credentials expire
|
||||
|
||||
**Pros**:
|
||||
- Zero configuration required
|
||||
- Quick setup
|
||||
- Automatic credential management
|
||||
|
||||
**Cons**:
|
||||
- Clients expire (default: 1 hour, configurable)
|
||||
- Must have dynamic client registration enabled on Nextcloud
|
||||
|
||||
**Configuration**: Skip to [Step 4](#step-4-configure-mcp-server) with minimal config.
|
||||
|
||||
---
|
||||
|
||||
### Mode B: Pre-configured Client (Production)
|
||||
|
||||
**Best for**: Production, long-running deployments, stable environments
|
||||
|
||||
**How it works**:
|
||||
- You manually register an OAuth client via Nextcloud CLI
|
||||
- Provide client credentials to MCP server via environment variables
|
||||
- Credentials don't expire
|
||||
|
||||
**Pros**:
|
||||
- Credentials don't expire
|
||||
- Stable for production
|
||||
- More control over client configuration
|
||||
- Better for audit trails
|
||||
|
||||
**Cons**:
|
||||
- Requires manual setup
|
||||
- Needs SSH/CLI access to Nextcloud server
|
||||
|
||||
**Setup**: Register a client via CLI:
|
||||
|
||||
```bash
|
||||
# SSH into Nextcloud server
|
||||
php occ oidc:create \
|
||||
--name="Nextcloud MCP Server" \
|
||||
--type=confidential \
|
||||
--redirect-uri="http://localhost:8000/oauth/callback"
|
||||
|
||||
# Example output:
|
||||
# Client ID: abc123xyz789
|
||||
# Client Secret: secret456def012
|
||||
|
||||
# Save these credentials for Step 4
|
||||
```
|
||||
|
||||
**Important**: Adjust `--redirect-uri` to match your MCP server URL:
|
||||
- Local: `http://localhost:8000/oauth/callback`
|
||||
- Remote: `http://your-server:8000/oauth/callback`
|
||||
- Custom port: `http://your-server:PORT/oauth/callback`
|
||||
|
||||
The redirect URI **must** be:
|
||||
```
|
||||
{NEXTCLOUD_MCP_SERVER_URL}/oauth/callback
|
||||
```
|
||||
|
||||
## Step 4: Configure MCP Server
|
||||
|
||||
Create or update your `.env` file with OAuth configuration.
|
||||
|
||||
### For Mode A (Automatic Registration)
|
||||
|
||||
```bash
|
||||
# Copy sample if needed
|
||||
cp env.sample .env
|
||||
|
||||
# Edit .env
|
||||
cat > .env << 'EOF'
|
||||
# Nextcloud Instance
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
|
||||
# Leave EMPTY for OAuth mode (do not set USERNAME/PASSWORD)
|
||||
NEXTCLOUD_USERNAME=
|
||||
NEXTCLOUD_PASSWORD=
|
||||
|
||||
# Optional: MCP server URL (for OAuth callbacks)
|
||||
NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000
|
||||
EOF
|
||||
```
|
||||
|
||||
### For Mode B (Pre-configured Client)
|
||||
|
||||
```bash
|
||||
# Copy sample if needed
|
||||
cp env.sample .env
|
||||
|
||||
# Edit .env
|
||||
cat > .env << 'EOF'
|
||||
# Nextcloud Instance
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
|
||||
# OAuth Client Credentials (from Step 3)
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=abc123xyz789
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=secret456def012
|
||||
|
||||
# MCP server URL (must match redirect URI)
|
||||
NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000
|
||||
|
||||
# Leave EMPTY for OAuth mode
|
||||
NEXTCLOUD_USERNAME=
|
||||
NEXTCLOUD_PASSWORD=
|
||||
EOF
|
||||
```
|
||||
|
||||
### Environment Variables Reference
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `NEXTCLOUD_HOST` | ✅ Yes | - | Full URL of Nextcloud instance |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_ID` | ⚠️ Mode B only | - | OAuth client ID |
|
||||
| `NEXTCLOUD_OIDC_CLIENT_SECRET` | ⚠️ Mode B only | - | OAuth client secret |
|
||||
| `NEXTCLOUD_MCP_SERVER_URL` | ⚠️ Optional | `http://localhost:8000` | MCP server URL for callbacks |
|
||||
| `NEXTCLOUD_USERNAME` | ❌ Must be empty | - | Leave empty for OAuth |
|
||||
| `NEXTCLOUD_PASSWORD` | ❌ Must be empty | - | Leave empty for OAuth |
|
||||
|
||||
See [Configuration Guide](configuration.md) for all options.
|
||||
|
||||
## Step 5: Start and Verify
|
||||
|
||||
### Load Environment Variables
|
||||
|
||||
```bash
|
||||
# Load from .env file
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
|
||||
# Verify key variables are set
|
||||
echo "NEXTCLOUD_HOST: $NEXTCLOUD_HOST"
|
||||
echo "NEXTCLOUD_MCP_SERVER_URL: $NEXTCLOUD_MCP_SERVER_URL"
|
||||
```
|
||||
|
||||
### Start MCP Server
|
||||
|
||||
```bash
|
||||
# Start with OAuth mode
|
||||
uv run nextcloud-mcp-server --oauth
|
||||
|
||||
# Or with custom options
|
||||
uv run nextcloud-mcp-server --oauth --port 8000 --log-level info
|
||||
```
|
||||
|
||||
### Verify Startup
|
||||
|
||||
Look for these success messages:
|
||||
|
||||
**For Mode A (Auto-registration)**:
|
||||
```
|
||||
INFO OAuth mode detected (NEXTCLOUD_USERNAME/PASSWORD not set)
|
||||
INFO Configuring MCP server for OAuth mode
|
||||
INFO Performing OIDC discovery: https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
✓ PKCE support validated: ['S256']
|
||||
INFO OIDC discovery successful
|
||||
INFO Attempting dynamic client registration...
|
||||
INFO Dynamic client registration successful
|
||||
INFO OAuth client ready: <client-id>...
|
||||
INFO Saved OAuth client credentials to SQLite database
|
||||
INFO OAuth initialization complete
|
||||
INFO MCP server ready at http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
**For Mode B (Pre-configured)**:
|
||||
```
|
||||
INFO OAuth mode detected (NEXTCLOUD_USERNAME/PASSWORD not set)
|
||||
INFO Configuring MCP server for OAuth mode
|
||||
INFO Performing OIDC discovery: https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
✓ PKCE support validated: ['S256']
|
||||
INFO OIDC discovery successful
|
||||
INFO Using pre-configured OAuth client: abc123xyz789
|
||||
INFO OAuth initialization complete
|
||||
INFO MCP server ready at http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
### Common Startup Issues
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| "OAuth mode requires NEXTCLOUD_HOST" | Set `NEXTCLOUD_HOST` in `.env` |
|
||||
| "OIDC discovery failed" | Verify Nextcloud URL and network connectivity |
|
||||
| "Dynamic registration failed" | Enable dynamic registration in OIDC app settings |
|
||||
| "PKCE validation failed" | See [Upstream Status](oauth-upstream-status.md) |
|
||||
|
||||
See [OAuth Troubleshooting](oauth-troubleshooting.md) for detailed solutions.
|
||||
|
||||
## Testing Authentication
|
||||
|
||||
### Test with MCP Inspector
|
||||
|
||||
The MCP Inspector provides a web UI for testing:
|
||||
|
||||
```bash
|
||||
# In a new terminal
|
||||
uv run mcp dev
|
||||
|
||||
# Opens browser at http://localhost:6272
|
||||
```
|
||||
|
||||
In the MCP Inspector UI:
|
||||
1. Enter server URL: `http://localhost:8000/mcp`
|
||||
2. Click **Connect**
|
||||
3. Complete OAuth flow in browser popup:
|
||||
- Login to Nextcloud
|
||||
- Authorize MCP server access
|
||||
- Redirected back to MCP Inspector
|
||||
4. Test tools:
|
||||
- Try `nc_notes_create_note`
|
||||
- Try `nc_notes_search_notes`
|
||||
- Try `nc_calendar_list_events`
|
||||
|
||||
### Test from Command Line
|
||||
|
||||
```bash
|
||||
# Get an OAuth token (you'll need to implement client flow or extract from browser)
|
||||
TOKEN="your_access_token_here"
|
||||
|
||||
# Test OCS API (should work)
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"$NEXTCLOUD_HOST/ocs/v2.php/cloud/capabilities?format=json" \
|
||||
-H "OCS-APIRequest: true"
|
||||
|
||||
# Test Notes API (requires upstream patch)
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"$NEXTCLOUD_HOST/apps/notes/api/v1/notes"
|
||||
```
|
||||
|
||||
### Verify Token Validation
|
||||
|
||||
Check MCP server logs for token validation:
|
||||
|
||||
```bash
|
||||
# Start server with debug logging
|
||||
uv run nextcloud-mcp-server --oauth --log-level debug
|
||||
|
||||
# Look for:
|
||||
# DEBUG Token validation via userinfo endpoint
|
||||
# DEBUG Token validated successfully for user: username
|
||||
```
|
||||
|
||||
## Production Recommendations
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
1. **Use Pre-configured Clients** (Mode B)
|
||||
- More stable
|
||||
- Better audit trails
|
||||
- No expiration issues
|
||||
|
||||
2. **Secure Credential Storage**
|
||||
```bash
|
||||
# Set restrictive permissions on environment file
|
||||
chmod 600 .env
|
||||
# Database permissions are handled automatically
|
||||
```
|
||||
|
||||
3. **Use HTTPS for MCP Server**
|
||||
- Especially important for remote access
|
||||
- Use reverse proxy (nginx, Apache) with SSL
|
||||
|
||||
4. **Restrict Redirect URIs**
|
||||
- Only register necessary redirect URIs
|
||||
- Use specific URLs (not wildcards)
|
||||
|
||||
### Deployment Considerations
|
||||
|
||||
1. **MCP Server URL**
|
||||
- Must be accessible to OAuth clients
|
||||
- Must match redirect URI registered with Nextcloud
|
||||
- For Docker: expose port and use correct host
|
||||
|
||||
2. **Network Configuration**
|
||||
- MCP server must reach Nextcloud (OIDC endpoints)
|
||||
- OAuth clients must reach MCP server (callbacks)
|
||||
- OAuth clients must reach Nextcloud (authorization flow)
|
||||
|
||||
3. **Process Management**
|
||||
- Use systemd, supervisord, or Docker for MCP server
|
||||
- Ensure automatic restart on failure
|
||||
- Monitor logs for OAuth errors
|
||||
|
||||
### Example Production Configs
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
nextcloud-mcp:
|
||||
image: ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
environment:
|
||||
NEXTCLOUD_HOST: https://your.nextcloud.instance.com
|
||||
NEXTCLOUD_OIDC_CLIENT_ID: ${NEXTCLOUD_OIDC_CLIENT_ID}
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET: ${NEXTCLOUD_OIDC_CLIENT_SECRET}
|
||||
NEXTCLOUD_MCP_SERVER_URL: http://your-server:8000
|
||||
volumes:
|
||||
- ./data:/app/data # For SQLite database persistence
|
||||
command: ["--oauth", "--transport", "streamable-http"]
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
#### Systemd Service
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Nextcloud MCP Server (OAuth)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mcp
|
||||
WorkingDirectory=/opt/nextcloud-mcp-server
|
||||
Environment="NEXTCLOUD_HOST=https://your.nextcloud.instance.com"
|
||||
Environment="NEXTCLOUD_OIDC_CLIENT_ID=abc123xyz789"
|
||||
Environment="NEXTCLOUD_OIDC_CLIENT_SECRET=secret456def012"
|
||||
Environment="NEXTCLOUD_MCP_SERVER_URL=http://your-server:8000"
|
||||
ExecStart=/opt/nextcloud-mcp-server/.venv/bin/nextcloud-mcp-server --oauth
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Monitoring and Maintenance
|
||||
|
||||
1. **Log Monitoring**
|
||||
```bash
|
||||
# Watch for OAuth errors
|
||||
tail -f /var/log/nextcloud-mcp/server.log | grep -i "oauth\|token"
|
||||
```
|
||||
|
||||
2. **Token Expiration** (Mode A only)
|
||||
- Monitor for "Stored client has expired" messages
|
||||
- Consider increasing expiration or switching to Mode B
|
||||
|
||||
3. **Upstream Patches**
|
||||
- Subscribe to [Upstream Status](oauth-upstream-status.md)
|
||||
- Plan to update when patches are merged
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For OAuth-specific issues, see [OAuth Troubleshooting](oauth-troubleshooting.md).
|
||||
|
||||
Common issues:
|
||||
- [OIDC discovery failed](oauth-troubleshooting.md#oidc-discovery-failed)
|
||||
- [Bearer token auth fails](oauth-troubleshooting.md#bearer-token-authentication-fails)
|
||||
- [Client expired](oauth-troubleshooting.md#client-expired)
|
||||
- [PKCE errors](oauth-troubleshooting.md#pkce-not-advertised)
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [OAuth Architecture](oauth-architecture.md) - Understand how OAuth works
|
||||
- [OAuth Troubleshooting](oauth-troubleshooting.md) - Solve common issues
|
||||
- [Upstream Status](oauth-upstream-status.md) - Track required patches
|
||||
- [Configuration](configuration.md) - All environment variables
|
||||
- [Running the Server](running.md) - Additional server options
|
||||
|
||||
## See Also
|
||||
|
||||
- [Authentication Overview](authentication.md) - OAuth vs BasicAuth comparison
|
||||
- [Quick Start Guide](quickstart-oauth.md) - 5-minute setup for development
|
||||
- [MCP Specification](https://spec.modelcontextprotocol.io/) - MCP protocol details
|
||||
- [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) - OAuth 2.0 Framework
|
||||
- [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636) - PKCE Extension
|
||||
@@ -1,642 +0,0 @@
|
||||
# OAuth Troubleshooting
|
||||
|
||||
This guide covers OAuth-specific issues and solutions for the Nextcloud MCP server.
|
||||
|
||||
For general troubleshooting, see [Troubleshooting Guide](troubleshooting.md).
|
||||
|
||||
## Quick Diagnosis
|
||||
|
||||
Start here to identify your issue:
|
||||
|
||||
| Symptom | Likely Cause | Quick Fix Link |
|
||||
|---------|--------------|----------------|
|
||||
| "OAuth mode requires NEXTCLOUD_HOST" | Missing environment variable | [Missing NEXTCLOUD_HOST](#missing-nextcloud_host) |
|
||||
| "OAuth mode requires client credentials OR dynamic registration" | OIDC apps not configured | [Missing OIDC Apps](#missing-or-misconfigured-oidc-apps) |
|
||||
| "PKCE support validation failed" | OIDC app doesn't advertise PKCE | [PKCE Not Advertised](#pkce-not-advertised) |
|
||||
| "Stored client has expired" | Dynamic client expired | [Client Expired](#client-expired) |
|
||||
| Only seeing Notes tools (7 instead of 90+) | Limited OAuth scopes granted | [Limited Scopes](#limited-scopes---only-seeing-notes-tools) |
|
||||
| HTTP 401 for Notes API | Bearer token patch missing | [Bearer Token Auth Fails](#bearer-token-authentication-fails) |
|
||||
| "OIDC discovery failed" | Network or configuration issue | [Discovery Failed](#oidc-discovery-failed) |
|
||||
| "Database error" on OAuth client storage | Database permissions issue | [Database Permission Error](#database-permission-error) |
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
### Missing NEXTCLOUD_HOST
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
OAuth mode requires NEXTCLOUD_HOST environment variable
|
||||
```
|
||||
|
||||
**Cause**: The `NEXTCLOUD_HOST` environment variable is not set or empty.
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Add to your `.env` file:
|
||||
```bash
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
```
|
||||
|
||||
2. Reload environment variables:
|
||||
```bash
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
```
|
||||
|
||||
3. Verify it's set:
|
||||
```bash
|
||||
echo $NEXTCLOUD_HOST
|
||||
# Should output: https://your.nextcloud.instance.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Missing or Misconfigured OIDC Apps
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
OAuth mode requires either client credentials OR dynamic client registration
|
||||
```
|
||||
|
||||
**Cause**: The required Nextcloud OIDC apps are either:
|
||||
- Not installed
|
||||
- Not enabled
|
||||
- Missing configuration
|
||||
|
||||
**Solution**:
|
||||
|
||||
**Step 1**: Verify both apps are installed:
|
||||
|
||||
```bash
|
||||
# Check installed apps
|
||||
php occ app:list | grep -E "oidc|user_oidc"
|
||||
|
||||
# Should show:
|
||||
# - oidc: enabled
|
||||
# - user_oidc: enabled
|
||||
```
|
||||
|
||||
If not installed:
|
||||
1. Open Nextcloud as administrator
|
||||
2. Navigate to **Apps** → **Security**
|
||||
3. Install **"OIDC"** (OIDC Identity Provider)
|
||||
4. Install **"OpenID Connect user backend"** (user_oidc)
|
||||
5. Enable both apps
|
||||
|
||||
**Step 2**: Enable dynamic client registration:
|
||||
|
||||
1. Go to **Settings** → **OIDC** (Administration)
|
||||
2. Enable **"Allow dynamic client registration"**
|
||||
|
||||
**Step 3**: Configure Bearer token validation:
|
||||
|
||||
```bash
|
||||
php occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean
|
||||
```
|
||||
|
||||
**Step 4**: Verify discovery endpoint:
|
||||
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration | jq '.registration_endpoint'
|
||||
|
||||
# Should output:
|
||||
# "https://your.nextcloud.instance.com/apps/oidc/register"
|
||||
```
|
||||
|
||||
**Alternative**: Use pre-configured client credentials:
|
||||
|
||||
```bash
|
||||
# Register client via CLI
|
||||
php occ oidc:create \
|
||||
--name="Nextcloud MCP Server" \
|
||||
--type=confidential \
|
||||
--redirect-uri="http://localhost:8000/oauth/callback"
|
||||
|
||||
# Add to .env
|
||||
echo "NEXTCLOUD_OIDC_CLIENT_ID=<client-id>" >> .env
|
||||
echo "NEXTCLOUD_OIDC_CLIENT_SECRET=<client-secret>" >> .env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Client Expired
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Stored client has expired
|
||||
```
|
||||
|
||||
**Cause**: Dynamically registered OAuth clients expire (default: 1 hour).
|
||||
|
||||
**Solution**:
|
||||
|
||||
**Option 1: Restart the Server** (Automatic re-registration)
|
||||
|
||||
```bash
|
||||
uv run nextcloud-mcp-server --oauth
|
||||
# Server automatically re-registers if credentials expired
|
||||
```
|
||||
|
||||
**Option 2: Use Pre-configured Credentials** (Recommended for production)
|
||||
|
||||
```bash
|
||||
# Register permanent client via Nextcloud CLI
|
||||
php occ oidc:create \
|
||||
--name="Nextcloud MCP Server" \
|
||||
--type=confidential \
|
||||
--redirect-uri="http://localhost:8000/oauth/callback"
|
||||
|
||||
# Add to .env
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=<from-output>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<from-output>
|
||||
```
|
||||
|
||||
Pre-configured clients don't expire.
|
||||
|
||||
**Option 3: Increase Expiration Time**
|
||||
|
||||
```bash
|
||||
# Via Nextcloud CLI (default: 3600 seconds = 1 hour)
|
||||
php occ config:app:set oidc expire_time --value "86400" # 24 hours
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Database Permission Error
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Permission denied when accessing SQLite database
|
||||
Database is locked
|
||||
```
|
||||
|
||||
**Cause**: The server cannot access the SQLite database file.
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Check database directory permissions
|
||||
ls -la /app/data/
|
||||
|
||||
# Ensure directory is writable
|
||||
chmod 755 /app/data
|
||||
|
||||
# Check if database file exists and has correct permissions
|
||||
ls -la /app/data/tokens.db
|
||||
chmod 644 /app/data/tokens.db
|
||||
|
||||
# If running in Docker, ensure volume is mounted correctly
|
||||
docker compose logs mcp-oauth | grep -i "database\|sqlite"
|
||||
```
|
||||
|
||||
**For Docker deployments**:
|
||||
Ensure the data directory is properly mounted as a volume:
|
||||
```yaml
|
||||
volumes:
|
||||
- ./data:/app/data # Persistent storage for SQLite database
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Discovery and Connection Issues
|
||||
|
||||
### OIDC Discovery Failed
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
OIDC discovery failed
|
||||
Cannot reach OIDC discovery endpoint
|
||||
```
|
||||
|
||||
**Cause**: The server cannot reach the Nextcloud OIDC discovery endpoint.
|
||||
|
||||
**Solution**:
|
||||
|
||||
**Step 1**: Verify Nextcloud URL is correct:
|
||||
|
||||
```bash
|
||||
echo $NEXTCLOUD_HOST
|
||||
# Should be full URL: https://your.nextcloud.instance.com
|
||||
```
|
||||
|
||||
**Step 2**: Test discovery endpoint manually:
|
||||
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
|
||||
# Should return JSON with OIDC configuration
|
||||
# {
|
||||
# "issuer": "https://your.nextcloud.instance.com",
|
||||
# "authorization_endpoint": "https://your.nextcloud.instance.com/apps/oidc/authorize",
|
||||
# ...
|
||||
# }
|
||||
```
|
||||
|
||||
**Step 3**: Check network connectivity:
|
||||
|
||||
```bash
|
||||
# Test basic connectivity
|
||||
ping your.nextcloud.instance.com
|
||||
|
||||
# Test HTTPS
|
||||
curl -I https://your.nextcloud.instance.com
|
||||
```
|
||||
|
||||
**Step 4**: Verify both OIDC apps are enabled:
|
||||
|
||||
```bash
|
||||
php occ app:list | grep -E "oidc|user_oidc"
|
||||
```
|
||||
|
||||
**Step 5**: Check firewall rules (if using Docker):
|
||||
|
||||
```bash
|
||||
# Check if MCP server can reach Nextcloud
|
||||
docker exec nextcloud-mcp-server curl https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Issues
|
||||
|
||||
### Bearer Token Authentication Fails
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
HTTP 401 Unauthorized when calling Nextcloud APIs
|
||||
```
|
||||
|
||||
**Symptoms**:
|
||||
- OCS APIs work (`/ocs/v2.php/cloud/capabilities`)
|
||||
- App APIs fail (`/apps/notes/api/`, `/apps/calendar/`, etc.)
|
||||
|
||||
**Cause**: The `user_oidc` app's CORS middleware interferes with Bearer token authentication for non-OCS endpoints.
|
||||
|
||||
**Solution**: Apply the Bearer token patch to `user_oidc` app.
|
||||
|
||||
See [Upstream Status](oauth-upstream-status.md#1-bearer-token-support-for-non-ocs-endpoints) for details.
|
||||
|
||||
**Quick Patch**:
|
||||
|
||||
```bash
|
||||
# SSH into Nextcloud server
|
||||
cd /path/to/nextcloud/apps/user_oidc
|
||||
|
||||
# Edit lib/User/Backend.php
|
||||
# Add this line before each return statement in getCurrentUserId() method:
|
||||
$this->session->set('app_api', true);
|
||||
|
||||
# Lines to modify: ~243, ~310, ~315, ~337
|
||||
```
|
||||
|
||||
**Test the fix**:
|
||||
|
||||
```bash
|
||||
# Get an OAuth token (from MCP client or test)
|
||||
TOKEN="your_access_token"
|
||||
|
||||
# Test Notes API
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
https://your.nextcloud.instance.com/apps/notes/api/v1/notes
|
||||
|
||||
# Should return notes JSON (not 401)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PKCE Not Advertised
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
ERROR: OIDC CONFIGURATION ERROR - Missing PKCE Support Advertisement
|
||||
⚠️ MCP clients (like Claude Code) WILL REJECT this provider!
|
||||
```
|
||||
|
||||
**Cause**: The OIDC discovery endpoint doesn't include `code_challenge_methods_supported` field.
|
||||
|
||||
**Impact**:
|
||||
- Some MCP clients may refuse to connect
|
||||
- Standards compliance issue (RFC 8414)
|
||||
- **Functionality still works** (PKCE is accepted, just not advertised)
|
||||
|
||||
**Solution**:
|
||||
|
||||
**Short-term**: The MCP server logs a warning but continues. OAuth flow still works.
|
||||
|
||||
**Long-term**: Update the `oidc` app to advertise PKCE support.
|
||||
|
||||
See [Upstream Status](oauth-upstream-status.md#2-pkce-support-advertisement-in-discovery) for tracking.
|
||||
|
||||
**Verify**:
|
||||
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration | jq '.code_challenge_methods_supported'
|
||||
|
||||
# Should return:
|
||||
# ["S256", "plain"]
|
||||
|
||||
# If null, PKCE isn't advertised (but still works)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Runtime Issues
|
||||
|
||||
### MCP Client Can't Authenticate
|
||||
|
||||
**Symptoms**:
|
||||
- Client connects but OAuth flow fails
|
||||
- Authorization redirects don't work
|
||||
- Token exchange fails
|
||||
|
||||
**Diagnosis**:
|
||||
|
||||
**Step 1**: Verify OAuth is configured correctly:
|
||||
|
||||
```bash
|
||||
uv run nextcloud-mcp-server --oauth --log-level debug
|
||||
```
|
||||
|
||||
Look for:
|
||||
```
|
||||
INFO OAuth initialization complete
|
||||
INFO MCP server ready at http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
**Step 2**: Check OIDC discovery:
|
||||
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
**Step 3**: Verify MCP server URL matches client expectations:
|
||||
|
||||
```bash
|
||||
echo $NEXTCLOUD_MCP_SERVER_URL
|
||||
# Should match the URL clients use to connect
|
||||
# Default: http://localhost:8000
|
||||
```
|
||||
|
||||
If MCP server is on a different host/port, update:
|
||||
```bash
|
||||
NEXTCLOUD_MCP_SERVER_URL=http://actual-host:actual-port
|
||||
```
|
||||
|
||||
**Step 4**: Check redirect URI configuration:
|
||||
|
||||
For pre-configured clients, ensure redirect URI matches:
|
||||
```bash
|
||||
# Client redirect URI should be:
|
||||
http://your-mcp-server-url/oauth/callback
|
||||
|
||||
# Example for local server:
|
||||
http://localhost:8000/oauth/callback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Tools Return 401 Errors
|
||||
|
||||
**Symptoms**:
|
||||
- OAuth flow completes successfully
|
||||
- Token is valid
|
||||
- MCP tools return 401 errors
|
||||
|
||||
**Cause**: Bearer token not working with Nextcloud APIs.
|
||||
|
||||
**Solution**: See [Bearer Token Authentication Fails](#bearer-token-authentication-fails) above.
|
||||
|
||||
---
|
||||
|
||||
### Limited Scopes - Only Seeing Notes Tools
|
||||
|
||||
**Symptoms**:
|
||||
- MCP client (e.g., Claude Code) successfully connects via OAuth
|
||||
- Only Notes tools are available (7 tools instead of 90+)
|
||||
- Token scopes show only `mcp:notes.read` and `mcp:notes.write`
|
||||
|
||||
**Cause**: During the OAuth consent flow, the user only granted access to Notes scopes, or the client only requested those scopes.
|
||||
|
||||
**Diagnosis**:
|
||||
|
||||
Check what scopes the client has been granted:
|
||||
|
||||
```bash
|
||||
# View registered clients and their allowed scopes
|
||||
php occ oidc:list | jq '.[] | select(.name | contains("Claude Code")) | {name, allowed_scopes}'
|
||||
```
|
||||
|
||||
Look for the client's `allowed_scopes` field. If it's empty or only contains notes scopes, that's the issue.
|
||||
|
||||
**Solution**:
|
||||
|
||||
**Option 1: Delete Client and Reconnect** (Recommended for MCP clients)
|
||||
|
||||
```bash
|
||||
# Find the client ID
|
||||
php occ oidc:list | jq '.[] | select(.name | contains("Claude Code")) | {name, client_id}'
|
||||
|
||||
# Delete the client
|
||||
php occ oidc:delete <client_id>
|
||||
|
||||
# Reconnect from Claude Code
|
||||
# This will trigger a new OAuth flow where you can grant all scopes
|
||||
```
|
||||
|
||||
When reconnecting, you'll see a consent screen listing all available scopes. Make sure to approve all the scopes you want the client to access.
|
||||
|
||||
**Option 2: Update Client Scopes via CLI**
|
||||
|
||||
```bash
|
||||
# Update allowed scopes for an existing client
|
||||
php occ oidc:update <client_id> \
|
||||
--allowed-scopes "openid profile email mcp:notes.read mcp:notes.write mcp:calendar.read mcp:calendar.write mcp:contacts.read mcp:contacts.write mcp:cookbook.read mcp:cookbook.write mcp:deck.read mcp:deck.write mcp:tables.read mcp:tables.write mcp:files.read mcp:files.write mcp:sharing.read mcp:sharing.write"
|
||||
|
||||
# User will need to reconnect to get new token with updated scopes
|
||||
```
|
||||
|
||||
**Verify Available Scopes**:
|
||||
|
||||
Check what scopes the MCP server advertises:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8001/.well-known/oauth-protected-resource | jq '.scopes_supported'
|
||||
|
||||
# Should show all 16 scope categories:
|
||||
# - openid
|
||||
# - mcp:notes.read, mcp:notes.write
|
||||
# - mcp:calendar.read, mcp:calendar.write
|
||||
# - mcp:contacts.read, mcp:contacts.write
|
||||
# - mcp:cookbook.read, mcp:cookbook.write
|
||||
# - mcp:deck.read, mcp:deck.write
|
||||
# - mcp:tables.read, mcp:tables.write
|
||||
# - mcp:files.read, mcp:files.write
|
||||
# - mcp:sharing.read, mcp:sharing.write
|
||||
```
|
||||
|
||||
**Understanding Scope Filtering**:
|
||||
|
||||
The MCP server dynamically filters tools based on the scopes in your access token:
|
||||
- Check server logs for: `✂️ JWT scope filtering: X/90 tools available for scopes: {...}`
|
||||
- This shows how many tools are visible vs total available
|
||||
- Each tool requires specific scopes (read and/or write)
|
||||
|
||||
**Available Scope Categories**:
|
||||
|
||||
| Scope Prefix | Nextcloud App | Read Operations | Write Operations |
|
||||
|--------------|---------------|-----------------|------------------|
|
||||
| `mcp:notes:*` | Notes | Get, search, list | Create, update, delete, append |
|
||||
| `mcp:calendar:*` | Calendar (CalDAV) | Get events, todos, calendars | Create/update/delete events, todos |
|
||||
| `mcp:contacts:*` | Contacts (CardDAV) | Get contacts, address books | Create/update/delete contacts |
|
||||
| `mcp:cookbook:*` | Cookbook | Get recipes, search | Create/update recipes |
|
||||
| `mcp:deck:*` | Deck | Get boards, cards | Create/update boards, cards |
|
||||
| `mcp:tables:*` | Tables | Get rows, tables | Create/update/delete rows |
|
||||
| `mcp:files:*` | Files (WebDAV) | List, read files | Upload, delete, move files |
|
||||
| `mcp:sharing:*` | Sharing | Get shares | Create/update shares |
|
||||
|
||||
---
|
||||
|
||||
## Switching Authentication Modes
|
||||
|
||||
### From BasicAuth to OAuth
|
||||
|
||||
```bash
|
||||
# 1. Remove or comment out USERNAME/PASSWORD in .env
|
||||
sed -i 's/^NEXTCLOUD_USERNAME/#NEXTCLOUD_USERNAME/' .env
|
||||
sed -i 's/^NEXTCLOUD_PASSWORD/#NEXTCLOUD_PASSWORD/' .env
|
||||
|
||||
# 2. Ensure NEXTCLOUD_HOST is set
|
||||
grep NEXTCLOUD_HOST .env
|
||||
|
||||
# 3. Restart server with OAuth
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
uv run nextcloud-mcp-server --oauth
|
||||
```
|
||||
|
||||
### From OAuth to BasicAuth
|
||||
|
||||
```bash
|
||||
# 1. Add USERNAME/PASSWORD to .env
|
||||
echo "NEXTCLOUD_USERNAME=your-username" >> .env
|
||||
echo "NEXTCLOUD_PASSWORD=your-password" >> .env
|
||||
|
||||
# 2. Restart server (BasicAuth auto-detected)
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
uv run nextcloud-mcp-server --no-oauth
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Debugging
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```bash
|
||||
uv run nextcloud-mcp-server --oauth --log-level debug
|
||||
```
|
||||
|
||||
Look for:
|
||||
- OIDC discovery details
|
||||
- Client registration attempts
|
||||
- Token validation logs
|
||||
- API request/response details
|
||||
|
||||
### Test Discovery Endpoint
|
||||
|
||||
```bash
|
||||
# Full discovery response
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration | jq
|
||||
|
||||
# Check specific fields
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration | jq '{
|
||||
issuer,
|
||||
authorization_endpoint,
|
||||
token_endpoint,
|
||||
userinfo_endpoint,
|
||||
registration_endpoint,
|
||||
code_challenge_methods_supported
|
||||
}'
|
||||
```
|
||||
|
||||
### Test Token Validation
|
||||
|
||||
```bash
|
||||
# Get userinfo with token
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
https://your.nextcloud.instance.com/apps/oidc/userinfo
|
||||
|
||||
# Should return user info:
|
||||
# {
|
||||
# "sub": "username",
|
||||
# "preferred_username": "username",
|
||||
# "name": "Display Name",
|
||||
# ...
|
||||
# }
|
||||
```
|
||||
|
||||
### Test Nextcloud API Access
|
||||
|
||||
```bash
|
||||
# Test OCS API (should work)
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"$NEXTCLOUD_HOST/ocs/v2.php/cloud/capabilities?format=json" \
|
||||
-H "OCS-APIRequest: true"
|
||||
|
||||
# Test app API (requires patch)
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"$NEXTCLOUD_HOST/apps/notes/api/v1/notes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you continue to experience issues:
|
||||
|
||||
### 1. Collect Diagnostic Information
|
||||
|
||||
```bash
|
||||
# Server version
|
||||
uv run nextcloud-mcp-server --version
|
||||
|
||||
# Python version
|
||||
python3 --version
|
||||
|
||||
# Server logs with debug
|
||||
uv run nextcloud-mcp-server --oauth --log-level debug 2>&1 | tee mcp-server.log
|
||||
|
||||
# OIDC discovery
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration > oidc-discovery.json
|
||||
|
||||
# Nextcloud version
|
||||
# Check in Nextcloud admin panel or:
|
||||
php occ -V
|
||||
```
|
||||
|
||||
### 2. Check Documentation
|
||||
|
||||
- [OAuth Architecture](oauth-architecture.md) - How OAuth works
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Configuration steps
|
||||
- [Upstream Status](oauth-upstream-status.md) - Required patches
|
||||
- [Configuration](configuration.md) - Environment variables
|
||||
|
||||
### 3. Open an Issue
|
||||
|
||||
If problems persist, [open an issue](https://github.com/cbcoutinho/nextcloud-mcp-server/issues) with:
|
||||
|
||||
- **Error messages** (full text)
|
||||
- **Server logs** (with `--log-level debug`)
|
||||
- **OIDC discovery response** (from curl command above)
|
||||
- **Nextcloud version**
|
||||
- **OIDC app versions** (`oidc` and `user_oidc`)
|
||||
- **Steps to reproduce**
|
||||
- **Environment details** (OS, Python version, Docker vs local)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [OAuth Quick Start](quickstart-oauth.md) - Get started quickly
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Detailed configuration
|
||||
- [OAuth Architecture](oauth-architecture.md) - Technical details
|
||||
- [Upstream Status](oauth-upstream-status.md) - Required patches
|
||||
- [General Troubleshooting](troubleshooting.md) - Non-OAuth issues
|
||||
@@ -1,300 +0,0 @@
|
||||
# OAuth Upstream Status
|
||||
|
||||
This document tracks the status of upstream patches and pull requests required for full OAuth functionality.
|
||||
|
||||
## Overview
|
||||
|
||||
The Nextcloud MCP Server's OAuth implementation relies on two Nextcloud apps:
|
||||
- **`oidc`** - OIDC Identity Provider (Authorization Server)
|
||||
- **`user_oidc`** - OpenID Connect user backend (Token validation)
|
||||
|
||||
While the core OAuth flow works, there are **pending upstream improvements** that enhance functionality and standards compliance.
|
||||
|
||||
## Required Patches
|
||||
|
||||
### 1. Bearer Token Support for Non-OCS Endpoints
|
||||
|
||||
**Status**: 🟡 **Patch Required** (Pending Upstream)
|
||||
|
||||
**Affected Component**: **Nextcloud core server** (`CORSMiddleware`)
|
||||
|
||||
**Issue**: Bearer token authentication fails for app-specific APIs (Notes, Calendar, etc.) with `401 Unauthorized` errors, even though OCS APIs work correctly.
|
||||
|
||||
**Root Cause**: The `CORSMiddleware` in Nextcloud core server logs out sessions when CSRF tokens are missing. Bearer token authentication creates a session (via `user_oidc` app), but doesn't include CSRF tokens (stateless authentication). The middleware detects the logged-in session without CSRF token and calls `session->logout()`, invalidating the request.
|
||||
|
||||
**Solution**: Allow Bearer token requests to bypass CORS/CSRF checks in `CORSMiddleware`, since Bearer tokens are stateless and don't require CSRF protection.
|
||||
|
||||
**Upstream PR**: [nextcloud/server#55878](https://github.com/nextcloud/server/pull/55878)
|
||||
|
||||
**Workaround**: Manually apply the patch to `lib/private/AppFramework/Middleware/Security/CORSMiddleware.php` in Nextcloud core server
|
||||
|
||||
**Impact**:
|
||||
- ✅ **Works**: OCS APIs (`/ocs/v2.php/cloud/capabilities`)
|
||||
- ❌ **Requires Patch**: App APIs (`/apps/notes/api/`, `/apps/calendar/`, etc.)
|
||||
|
||||
**Files Modified**: `lib/private/AppFramework/Middleware/Security/CORSMiddleware.php` in **Nextcloud core server**
|
||||
|
||||
**Patch Summary**:
|
||||
```php
|
||||
// Allow Bearer token authentication for CORS requests
|
||||
// Bearer tokens are stateless and don't require CSRF protection
|
||||
$authorizationHeader = $this->request->getHeader('Authorization');
|
||||
if (!empty($authorizationHeader) && str_starts_with($authorizationHeader, 'Bearer ')) {
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
This is added before the CSRF check at line ~73 in `CORSMiddleware.php`.
|
||||
|
||||
---
|
||||
|
||||
### 2. JWT Token Support, Introspection, and Scope Validation
|
||||
|
||||
**Status**: ✅ **Complete** (Merged Upstream)
|
||||
|
||||
**Affected Component**: `oidc` app
|
||||
|
||||
**Issue**: The OIDC app needed support for JWT tokens, token introspection, and enhanced scope validation for fine-grained authorization.
|
||||
|
||||
**Resolution**: Complete JWT and scope validation support has been implemented and merged:
|
||||
|
||||
**Upstream PR**: [H2CK/oidc#585](https://github.com/H2CK/oidc/pull/585) - ✅ **Merged**
|
||||
- **Changes**:
|
||||
- JWT token generation and validation
|
||||
- Token introspection endpoint (RFC 7662)
|
||||
- Enhanced scope validation and parsing
|
||||
- Custom scope support for Nextcloud apps
|
||||
- **Status**: Merged and available in v1.10.0+ of the `oidc` app
|
||||
|
||||
---
|
||||
|
||||
### 3. User Consent Management
|
||||
|
||||
**Status**: ✅ **Complete** (Merged Upstream)
|
||||
|
||||
**Affected Component**: `oidc` app
|
||||
|
||||
**Issue**: The OIDC app needed proper user consent management for OAuth authorization flows.
|
||||
|
||||
**Resolution**: Complete user consent management has been implemented and merged:
|
||||
|
||||
**Upstream PR**: [H2CK/oidc#586](https://github.com/H2CK/oidc/pull/586) - ✅ **Merged**
|
||||
- **Changes**:
|
||||
- User consent UI for OAuth authorization
|
||||
- Consent expiration and cleanup
|
||||
- Admin control for user consent settings
|
||||
- Consent tracking and management
|
||||
- **Status**: Merged and available in v1.11.0+ of the `oidc` app
|
||||
|
||||
---
|
||||
|
||||
### 4. PKCE Support (RFC 7636)
|
||||
|
||||
**Status**: ✅ **Complete** (Merged Upstream)
|
||||
|
||||
**Affected Component**: `oidc` app
|
||||
|
||||
**Issue**: The OIDC app lacked PKCE (Proof Key for Code Exchange) implementation per RFC 7636.
|
||||
|
||||
**Resolution**: Full PKCE support has been implemented and merged upstream into the `oidc` app:
|
||||
|
||||
**Authorization Endpoint** (`/authorize`):
|
||||
- Accepts `code_challenge` and `code_challenge_method` parameters
|
||||
- Validates code_challenge format (43-128 characters, unreserved chars only)
|
||||
- Supports both `S256` (SHA-256) and `plain` challenge methods
|
||||
- Stores challenge and method in database for later verification
|
||||
|
||||
**Token Endpoint** (`/token`):
|
||||
- Accepts `code_verifier` parameter
|
||||
- Verifies code_verifier against stored code_challenge using proper algorithm
|
||||
- Uses constant-time comparison to prevent timing attacks
|
||||
- Enforces code_verifier requirement when PKCE was used in authorization
|
||||
|
||||
**Discovery Document**:
|
||||
```json
|
||||
{
|
||||
"code_challenge_methods_supported": ["S256", "plain"]
|
||||
}
|
||||
```
|
||||
|
||||
**Database**:
|
||||
- New columns: `code_challenge` and `code_challenge_method` in `oc_oauth2_access_tokens`
|
||||
- Migration included for existing installations
|
||||
|
||||
**Why It Mattered**:
|
||||
- MCP specification requires PKCE with S256 code challenge method
|
||||
- RFC 7636 PKCE provides security for public clients (no client secret)
|
||||
- RFC 8414 states that absence of `code_challenge_methods_supported` means PKCE is **not supported**
|
||||
- Prevents authorization code interception attacks
|
||||
|
||||
**Upstream PR**: [H2CK/oidc#584](https://github.com/H2CK/oidc/pull/584) - ✅ **Merged 2025-10-20**
|
||||
- **Changes**: Complete PKCE implementation (+194 lines)
|
||||
- Authorization flow with code_challenge validation
|
||||
- Token exchange with code_verifier verification
|
||||
- Database schema updates
|
||||
- Discovery document updates
|
||||
- **Status**: Merged and available in v1.10.0+ of the `oidc` app
|
||||
|
||||
---
|
||||
|
||||
## Upstream PRs Status
|
||||
|
||||
| PR/Issue | Component | Status | Priority | Notes |
|
||||
|----------|-----------|--------|----------|-------|
|
||||
| [server#55878](https://github.com/nextcloud/server/pull/55878) | Nextcloud core server | 🟡 Open | High | CORSMiddleware patch for Bearer tokens |
|
||||
| [H2CK/oidc#586](https://github.com/H2CK/oidc/pull/586) | `oidc` | ✅ Merged | Medium | ✅ User consent complete (v1.11.0+) |
|
||||
| [H2CK/oidc#585](https://github.com/H2CK/oidc/pull/585) | `oidc` | ✅ Merged | Medium | ✅ JWT tokens, introspection, scope validation (v1.10.0+) |
|
||||
| [H2CK/oidc#584](https://github.com/H2CK/oidc/pull/584) | `oidc` | ✅ Merged | ~~High~~ | ✅ PKCE support (RFC 7636) (v1.10.0+) |
|
||||
|
||||
## What Works Without Patches
|
||||
|
||||
The following functionality works **out of the box** without any patches:
|
||||
|
||||
✅ **OAuth Flow** (requires `oidc` app v1.10.0+):
|
||||
- OIDC discovery with full PKCE support (RFC 7636)
|
||||
- Dynamic client registration
|
||||
- Authorization code flow with PKCE (S256 and plain methods)
|
||||
- Token exchange with code_verifier verification
|
||||
- User consent management
|
||||
- Userinfo endpoint
|
||||
|
||||
✅ **Token Features** (requires `oidc` app v1.10.0+):
|
||||
- JWT token generation and validation
|
||||
- Token introspection endpoint (RFC 7662)
|
||||
- Enhanced scope validation and parsing
|
||||
- Custom scope support for Nextcloud apps
|
||||
|
||||
✅ **MCP Server as Resource Server**:
|
||||
- Token validation via userinfo
|
||||
- Per-user client instances
|
||||
- Token caching
|
||||
- Scope-based authorization
|
||||
|
||||
✅ **Nextcloud OCS APIs**:
|
||||
- Capabilities endpoint
|
||||
- All OCS-based APIs
|
||||
|
||||
## What Requires Patches
|
||||
|
||||
The following functionality requires upstream patches:
|
||||
|
||||
🟡 **App-Specific APIs** (Requires Nextcloud core server CORSMiddleware patch):
|
||||
- Notes API (`/apps/notes/api/`)
|
||||
- Calendar API (CalDAV)
|
||||
- Contacts API (CardDAV)
|
||||
- Deck API
|
||||
- Tables API
|
||||
- Custom app APIs
|
||||
|
||||
✅ **Standards Compliance**: Now complete with `oidc` app v1.10.0+
|
||||
- ✅ Full RFC 8414 compliance (PKCE advertisement)
|
||||
- ✅ MCP client compatibility guarantee
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
### For Development/Testing
|
||||
|
||||
If the upstream PRs are not yet merged, you can apply patches manually:
|
||||
|
||||
#### 1. Apply Bearer Token Patch
|
||||
|
||||
```bash
|
||||
# SSH into Nextcloud server
|
||||
cd /path/to/nextcloud/apps/user_oidc
|
||||
|
||||
# Download and apply patch
|
||||
# (Patch file to be created once PR is ready)
|
||||
wget https://github.com/nextcloud/user_oidc/pull/XXXX.patch
|
||||
git apply XXXX.patch
|
||||
|
||||
# Or manually edit lib/User/Backend.php
|
||||
# Add this line before each return statement in getCurrentUserId():
|
||||
# $this->session->set('app_api', true);
|
||||
```
|
||||
|
||||
#### 2. Verify Installation
|
||||
|
||||
```bash
|
||||
# Test with OAuth token
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
https://your.nextcloud.com/apps/notes/api/v1/notes
|
||||
|
||||
# Should return notes JSON (not 401)
|
||||
```
|
||||
|
||||
### For Production
|
||||
|
||||
**Recommendation**: Wait for upstream PRs to be merged and included in official Nextcloud releases before deploying OAuth in production.
|
||||
|
||||
**Alternative**: Use a patched version of `user_oidc` app in your deployment:
|
||||
1. Fork the `user_oidc` app
|
||||
2. Apply the required patches
|
||||
3. Install your patched version
|
||||
4. Document the changes for your team
|
||||
|
||||
## Testing
|
||||
|
||||
The integration test suite validates OAuth functionality:
|
||||
|
||||
```bash
|
||||
# Start OAuth-enabled MCP server
|
||||
docker-compose up --build -d mcp-oauth
|
||||
|
||||
# Run comprehensive OAuth tests
|
||||
uv run pytest tests/client/test_oauth_playwright.py --browser firefox -v
|
||||
|
||||
# Tests verify:
|
||||
# - OAuth flow completion
|
||||
# - Token validation
|
||||
# - MCP tool calls with Bearer tokens
|
||||
# - Notes API access (requires patch)
|
||||
```
|
||||
|
||||
## Monitoring Upstream Progress
|
||||
|
||||
To track progress on remaining issues:
|
||||
|
||||
1. **Watch the upstream repository**:
|
||||
- [nextcloud/server](https://github.com/nextcloud/server)
|
||||
|
||||
2. **Subscribe to the CORSMiddleware PR**:
|
||||
- [server#55878](https://github.com/nextcloud/server/pull/55878) - CORSMiddleware Bearer token support
|
||||
|
||||
3. **Check Nextcloud server release notes** for mentions of:
|
||||
- Bearer token authentication improvements
|
||||
- CORS middleware enhancements
|
||||
- OAuth/OIDC API compatibility
|
||||
|
||||
4. **Completed upstream work** (no monitoring needed):
|
||||
- ✅ [H2CK/oidc#584](https://github.com/H2CK/oidc/pull/584) - PKCE support (v1.10.0+)
|
||||
- ✅ [H2CK/oidc#585](https://github.com/H2CK/oidc/pull/585) - JWT, introspection, scopes (v1.10.0+)
|
||||
- ✅ [H2CK/oidc#586](https://github.com/H2CK/oidc/pull/586) - User consent (v1.11.0+)
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to help get these patches merged?
|
||||
|
||||
1. **Test the patches**: Run the integration tests and report results
|
||||
2. **Review PRs**: Provide feedback on upstream pull requests
|
||||
3. **Document issues**: Report any problems or edge cases
|
||||
4. **Contribute code**: Submit improvements or fixes to upstream
|
||||
|
||||
## Timeline Expectations
|
||||
|
||||
**Best Case**: PRs merged in next Nextcloud minor release (est. 3-6 months)
|
||||
|
||||
**Realistic**: PRs reviewed and merged within 6-12 months
|
||||
|
||||
**Meanwhile**: Use the workarounds documented in this guide
|
||||
|
||||
## See Also
|
||||
|
||||
- [OAuth Architecture](oauth-architecture.md) - How OAuth works in this implementation
|
||||
- [OAuth Troubleshooting](oauth-troubleshooting.md) - Common issues and solutions
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Configuration instructions
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-02
|
||||
|
||||
**Next Review**: When Nextcloud server CORSMiddleware PR has activity
|
||||
@@ -1,163 +0,0 @@
|
||||
# OAuth Quick Start Guide
|
||||
|
||||
Get up and running with OAuth authentication in 5 minutes.
|
||||
|
||||
## Prerequisites Checklist
|
||||
|
||||
Before you begin, ensure you have:
|
||||
|
||||
- [ ] Nextcloud instance with **administrator access**
|
||||
- [ ] Nextcloud version 28 or later
|
||||
- [ ] Python 3.11+ installed
|
||||
- [ ] `uv` package manager installed ([installation instructions](https://docs.astral.sh/uv/getting-started/installation/))
|
||||
|
||||
## Step 1: Install Nextcloud Apps
|
||||
|
||||
Install **both** required apps in your Nextcloud instance:
|
||||
|
||||
1. Open Nextcloud as administrator
|
||||
2. Navigate to **Apps** → **Security**
|
||||
3. Install:
|
||||
- **OIDC** (OIDC Identity Provider app)
|
||||
- **OpenID Connect user backend** (user_oidc app)
|
||||
4. Enable both apps
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `user_oidc` app requires an upstream patch for Bearer token support. See [Upstream Status](oauth-upstream-status.md) for details. The functionality works, but the PR is pending.
|
||||
|
||||
## Step 2: Configure Nextcloud OIDC
|
||||
|
||||
Enable dynamic client registration and Bearer token validation:
|
||||
|
||||
### Via Web UI
|
||||
|
||||
1. Go to **Settings** → **OIDC** (Administration settings)
|
||||
2. Enable **"Allow dynamic client registration"**
|
||||
|
||||
### Via CLI (Required)
|
||||
|
||||
SSH into your Nextcloud server and run:
|
||||
|
||||
```bash
|
||||
# Enable Bearer token validation
|
||||
php occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean
|
||||
```
|
||||
|
||||
## Step 3: Install MCP Server
|
||||
|
||||
Clone and install the MCP server:
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/cbcoutinho/nextcloud-mcp-server.git
|
||||
cd nextcloud-mcp-server
|
||||
|
||||
# Install dependencies
|
||||
uv sync
|
||||
```
|
||||
|
||||
## Step 4: Configure Environment
|
||||
|
||||
Create a `.env` file with minimal configuration:
|
||||
|
||||
```bash
|
||||
# Copy sample
|
||||
cp env.sample .env
|
||||
|
||||
# Edit .env and set:
|
||||
NEXTCLOUD_HOST=https://your.nextcloud.instance.com
|
||||
|
||||
# IMPORTANT: Leave these EMPTY for OAuth mode
|
||||
NEXTCLOUD_USERNAME=
|
||||
NEXTCLOUD_PASSWORD=
|
||||
```
|
||||
|
||||
## Step 5: Start the Server
|
||||
|
||||
Load environment variables and start the server:
|
||||
|
||||
```bash
|
||||
# Load environment
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
|
||||
# Start server with OAuth
|
||||
uv run nextcloud-mcp-server --oauth
|
||||
```
|
||||
|
||||
Look for this success message:
|
||||
|
||||
```
|
||||
✓ PKCE support validated: ['S256']
|
||||
INFO OAuth initialization complete
|
||||
INFO MCP server ready at http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
## Step 6: Test with MCP Inspector
|
||||
|
||||
Open a new terminal and test the connection:
|
||||
|
||||
```bash
|
||||
# Start MCP Inspector
|
||||
uv run mcp dev
|
||||
```
|
||||
|
||||
This opens your browser. In the MCP Inspector UI:
|
||||
|
||||
1. Enter server URL: `http://127.0.0.1:8000/mcp`
|
||||
2. Click **Connect**
|
||||
3. Complete the OAuth flow in the browser popup
|
||||
4. After authorization, you'll see available tools and resources
|
||||
|
||||
Test a tool by trying:
|
||||
- **Tool**: `nc_notes_create_note`
|
||||
- **Title**: "Test Note"
|
||||
- **Content**: "Hello from MCP!"
|
||||
- **Category**: "Notes"
|
||||
|
||||
## Troubleshooting Quick Fixes
|
||||
|
||||
### PKCE Error
|
||||
|
||||
If you see:
|
||||
```
|
||||
ERROR: OIDC CONFIGURATION ERROR - Missing PKCE Support Advertisement
|
||||
```
|
||||
|
||||
**Fix**: The Nextcloud OIDC app needs to be updated to advertise PKCE support. See [Upstream Status](oauth-upstream-status.md) for the required PR.
|
||||
|
||||
### 401 Unauthorized for Notes API
|
||||
|
||||
If OAuth works but Notes API returns 401:
|
||||
|
||||
**Fix**: The `user_oidc` app needs the Bearer token patch. See [Upstream Status](oauth-upstream-status.md) for details.
|
||||
|
||||
### Can't Reach OIDC Discovery Endpoint
|
||||
|
||||
**Fix**: Verify your Nextcloud URL is correct and accessible:
|
||||
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [OAuth Setup Guide](oauth-setup.md) - Detailed configuration options
|
||||
- [OAuth Architecture](oauth-architecture.md) - How it works under the hood
|
||||
- [OAuth Troubleshooting](oauth-troubleshooting.md) - Common issues and solutions
|
||||
- [Configuration](configuration.md) - All environment variables
|
||||
|
||||
## Development vs Production
|
||||
|
||||
This quick start uses **automatic client registration** which is perfect for:
|
||||
- Development
|
||||
- Testing
|
||||
- Quick deployments
|
||||
|
||||
For **production deployments**, consider:
|
||||
1. Pre-registering OAuth client manually
|
||||
2. Using dedicated client credentials that don't expire
|
||||
3. See [OAuth Setup Guide](oauth-setup.md) for production configuration
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Check [OAuth Troubleshooting](oauth-troubleshooting.md) or [open an issue](https://github.com/cbcoutinho/nextcloud-mcp-server/issues).
|
||||
+3
-2
@@ -8,7 +8,7 @@ Before running the server:
|
||||
|
||||
1. **Install the server** - See [Installation Guide](installation.md)
|
||||
2. **Configure environment** - See [Configuration Guide](configuration.md)
|
||||
3. **Set up authentication** - See [OAuth Setup](oauth-setup.md) or [Authentication](authentication.md)
|
||||
3. **Set up authentication** - See [Authentication](authentication.md) (multi-user deployments: see [Login Flow v2](login-flow-v2.md))
|
||||
|
||||
---
|
||||
|
||||
@@ -425,6 +425,7 @@ See [Troubleshooting OAuth](troubleshooting.md) for detailed OAuth troubleshooti
|
||||
## See Also
|
||||
|
||||
- [Configuration Guide](configuration.md) - Environment variables
|
||||
- [OAuth Setup](oauth-setup.md) - OAuth authentication setup
|
||||
- [Authentication](authentication.md) - Authentication modes
|
||||
- [Login Flow v2](login-flow-v2.md) - Recommended multi-user setup
|
||||
- [Troubleshooting](troubleshooting.md) - Common issues and solutions
|
||||
- [Installation](installation.md) - Installing the server
|
||||
|
||||
@@ -911,7 +911,7 @@ For detailed observability setup, see [docs/observability.md](observability.md).
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- **[OAuth Architecture](oauth-architecture.md)** - OAuth flows, scopes, token management
|
||||
- **[Login Flow v2](login-flow-v2.md)** - OAuth flows, scopes, token management
|
||||
- **[Comparison with Context Agent](comparison-context-agent.md)** - When to use Nextcloud MCP Server vs Context Agent
|
||||
|
||||
---
|
||||
|
||||
+41
-283
@@ -2,7 +2,7 @@
|
||||
|
||||
This guide covers common issues and solutions for the Nextcloud MCP server.
|
||||
|
||||
> **OAuth-specific issues?** See the dedicated [OAuth Troubleshooting Guide](oauth-troubleshooting.md) for OAuth authentication problems, OIDC discovery issues, token validation failures, and more.
|
||||
> **Multi-user / Login Flow v2 issues?** See the [Login Flow v2 troubleshooting section](login-flow-v2.md#troubleshooting) for app-password storage, provisioning loops, and OAuth issuer problems.
|
||||
|
||||
> **Upgrading from v0.57.x?** See the [Configuration Migration Guide](configuration-migration-v2.md) for help with new variable names.
|
||||
|
||||
@@ -69,7 +69,7 @@ ENABLE_BACKGROUND_OPERATIONS=true
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
ValueError: Invalid MCP_DEPLOYMENT_MODE: 'oauth'. Valid values: single_user_basic, multi_user_basic, oauth_single_audience, oauth_token_exchange
|
||||
ValueError: Invalid MCP_DEPLOYMENT_MODE: 'oauth'. Valid values: single_user_basic, multi_user_basic, login_flow_v2
|
||||
```
|
||||
|
||||
**Cause:** Invalid value for `MCP_DEPLOYMENT_MODE`.
|
||||
@@ -77,11 +77,9 @@ ValueError: Invalid MCP_DEPLOYMENT_MODE: 'oauth'. Valid values: single_user_basi
|
||||
**Solution:**
|
||||
Use one of the valid mode values:
|
||||
```bash
|
||||
# Correct values:
|
||||
MCP_DEPLOYMENT_MODE=single_user_basic # Single-user with username/password
|
||||
MCP_DEPLOYMENT_MODE=multi_user_basic # Multi-user BasicAuth
|
||||
MCP_DEPLOYMENT_MODE=oauth_single_audience # OAuth (recommended)
|
||||
MCP_DEPLOYMENT_MODE=oauth_token_exchange # OAuth with token exchange
|
||||
MCP_DEPLOYMENT_MODE=single_user_basic # Single-user with username/app password
|
||||
MCP_DEPLOYMENT_MODE=multi_user_basic # Multi-user BasicAuth pass-through
|
||||
MCP_DEPLOYMENT_MODE=login_flow_v2 # Multi-user via Login Flow v2 (recommended)
|
||||
```
|
||||
|
||||
Or remove `MCP_DEPLOYMENT_MODE` to use automatic detection.
|
||||
@@ -92,7 +90,7 @@ Or remove `MCP_DEPLOYMENT_MODE` to use automatic detection.
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: [oauth_single_audience] TOKEN_ENCRYPTION_KEY is required when ENABLE_SEMANTIC_SEARCH is enabled
|
||||
Error: [login_flow_v2] TOKEN_ENCRYPTION_KEY is required when ENABLE_SEMANTIC_SEARCH is enabled
|
||||
```
|
||||
|
||||
**Cause:** In multi-user modes, semantic search automatically enables background operations, which require encrypted token storage.
|
||||
@@ -107,8 +105,6 @@ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().d
|
||||
# Add to .env:
|
||||
TOKEN_ENCRYPTION_KEY=<generated-key>
|
||||
TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=your-client-id # Required for app password retrieval
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=your-client-secret
|
||||
```
|
||||
|
||||
**Why this happens:**
|
||||
@@ -143,234 +139,19 @@ The server will use the new name and ignore the old one, but it's cleaner to rem
|
||||
|
||||
---
|
||||
|
||||
## OAuth Issues (Quick Reference)
|
||||
## Multi-User / Login Flow v2 Issues
|
||||
|
||||
### Issue: "OAuth mode requires NEXTCLOUD_HOST environment variable"
|
||||
For multi-user deployment issues — provisioning loops, app-password storage, OAuth issuer endpoints, scope enforcement — see the [Login Flow v2 troubleshooting section](login-flow-v2.md#troubleshooting).
|
||||
|
||||
**Cause:** The `NEXTCLOUD_HOST` environment variable is not set or empty.
|
||||
|
||||
**Solution:**
|
||||
### Switching deployment modes
|
||||
|
||||
```bash
|
||||
# Ensure NEXTCLOUD_HOST is set in your .env file
|
||||
echo "NEXTCLOUD_HOST=https://your.nextcloud.instance.com" >> .env
|
||||
|
||||
# Load environment variables
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
|
||||
# Verify it's set
|
||||
echo $NEXTCLOUD_HOST
|
||||
# To Single-User BasicAuth: set NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD
|
||||
# To Multi-User BasicAuth pass-through: ENABLE_MULTI_USER_BASIC_AUTH=true (no creds)
|
||||
# To Login Flow v2: ENABLE_LOGIN_FLOW=true (no creds)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue: "OAuth mode requires either client credentials OR dynamic client registration"
|
||||
|
||||
**Cause:** The required Nextcloud OIDC apps are either:
|
||||
1. Not installed (both `oidc` and `user_oidc` apps are required)
|
||||
2. Don't have dynamic client registration enabled
|
||||
3. Aren't providing a registration endpoint
|
||||
|
||||
**Solution:**
|
||||
|
||||
**Option 1: Enable dynamic client registration**
|
||||
|
||||
1. Verify **both** OIDC apps are installed:
|
||||
- Navigate to Nextcloud **Apps** → **Security**
|
||||
- Install **"OIDC"** (OIDC Identity Provider app) if not present
|
||||
- Install **"OpenID Connect user backend"** (user_oidc app) if not present
|
||||
|
||||
2. Enable dynamic client registration:
|
||||
- Go to **Settings** → **OIDC** (Administration)
|
||||
- Enable "Allow dynamic client registration"
|
||||
|
||||
3. Configure Bearer token validation:
|
||||
```bash
|
||||
# Required for user_oidc app to validate tokens
|
||||
php occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean
|
||||
```
|
||||
|
||||
3. Verify the registration endpoint exists:
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration | jq '.registration_endpoint'
|
||||
# Should output: "https://your.nextcloud.instance.com/apps/oidc/register"
|
||||
```
|
||||
|
||||
**Option 2: Provide pre-configured credentials**
|
||||
|
||||
Register a client and add credentials to `.env`:
|
||||
|
||||
```bash
|
||||
# On your Nextcloud server
|
||||
php occ oidc:create \
|
||||
--name="Nextcloud MCP Server" \
|
||||
--type=confidential \
|
||||
--redirect-uri="http://localhost:8000/oauth/callback"
|
||||
|
||||
# Add to .env
|
||||
echo "NEXTCLOUD_OIDC_CLIENT_ID=<from-output>" >> .env
|
||||
echo "NEXTCLOUD_OIDC_CLIENT_SECRET=<from-output>" >> .env
|
||||
```
|
||||
|
||||
See [OAuth Setup Guide](oauth-setup.md) for detailed instructions.
|
||||
|
||||
---
|
||||
|
||||
### Issue: "Stored client has expired"
|
||||
|
||||
**Cause:** Dynamically registered OAuth clients expire (default: 1 hour).
|
||||
|
||||
**Solution:**
|
||||
|
||||
**Option 1: Restart the server** (automatic re-registration)
|
||||
|
||||
```bash
|
||||
# Server checks credentials at startup and re-registers if expired
|
||||
uv run nextcloud-mcp-server --oauth
|
||||
```
|
||||
|
||||
**Option 2: Use pre-configured credentials** (recommended for production)
|
||||
|
||||
```bash
|
||||
# Register permanent client via Nextcloud CLI
|
||||
php occ oidc:create \
|
||||
--name="Nextcloud MCP Server" \
|
||||
--type=confidential \
|
||||
--redirect-uri="http://localhost:8000/oauth/callback"
|
||||
|
||||
# Add to .env
|
||||
NEXTCLOUD_OIDC_CLIENT_ID=<from-output>
|
||||
NEXTCLOUD_OIDC_CLIENT_SECRET=<from-output>
|
||||
```
|
||||
|
||||
**Option 3: Increase expiration time**
|
||||
|
||||
```bash
|
||||
# Via Nextcloud occ command (default: 3600 seconds)
|
||||
php occ config:app:set oidc expire_time --value "86400" # 24 hours
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue: "HTTP 401 Unauthorized" when calling Nextcloud APIs
|
||||
|
||||
**Cause:** OAuth Bearer tokens may not work with certain Nextcloud endpoints due to session handling in the CORS middleware.
|
||||
|
||||
**Background:** The `user_oidc` app's CORS middleware interferes with Bearer token authentication for non-OCS endpoints (like Notes API). This affects app-specific APIs but not OCS APIs.
|
||||
|
||||
**Solution:**
|
||||
|
||||
A patch for the `user_oidc` app is required to fix Bearer token support. See [oauth2-bearer-token-session-issue.md](oauth2-bearer-token-session-issue.md) for:
|
||||
- Detailed explanation of the issue
|
||||
- Patch to apply to the `user_oidc` app
|
||||
- Link to upstream pull request
|
||||
|
||||
**Affected endpoints:**
|
||||
- Notes API (`/apps/notes/api/`)
|
||||
- Other app-specific endpoints
|
||||
|
||||
**Unaffected endpoints:**
|
||||
- OCS APIs (`/ocs/v2.php/`)
|
||||
- Capabilities endpoint
|
||||
|
||||
---
|
||||
|
||||
### Issue: "Permission denied" or "Database is locked" when accessing OAuth client storage
|
||||
|
||||
**Cause:** The server cannot access the SQLite database for OAuth client credentials storage.
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Check database directory permissions
|
||||
ls -la data/
|
||||
|
||||
# Ensure directory is writable
|
||||
chmod 755 data/
|
||||
|
||||
# Check if database file exists and has correct permissions
|
||||
ls -la data/tokens.db
|
||||
chmod 644 data/tokens.db
|
||||
|
||||
# For Docker deployments, ensure volume is mounted correctly:
|
||||
# docker-compose.yml should have:
|
||||
# volumes:
|
||||
# - ./data:/app/data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue: "OIDC discovery failed" or "Cannot reach OIDC discovery endpoint"
|
||||
|
||||
**Cause:** The server cannot reach the Nextcloud OIDC discovery endpoint.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify the Nextcloud URL is correct:
|
||||
```bash
|
||||
echo $NEXTCLOUD_HOST
|
||||
# Should be the full URL: https://your.nextcloud.instance.com
|
||||
```
|
||||
|
||||
2. Test the discovery endpoint manually:
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
# Should return JSON with OIDC configuration
|
||||
```
|
||||
|
||||
3. Check network connectivity:
|
||||
```bash
|
||||
ping your.nextcloud.instance.com
|
||||
```
|
||||
|
||||
4. Verify **both** OIDC apps are installed and enabled in Nextcloud:
|
||||
- `oidc` - OIDC Identity Provider
|
||||
- `user_oidc` - OpenID Connect user backend
|
||||
|
||||
5. Check firewall rules if using Docker
|
||||
|
||||
---
|
||||
|
||||
### Switching Between OAuth and BasicAuth
|
||||
|
||||
#### To switch from BasicAuth to OAuth:
|
||||
|
||||
```bash
|
||||
# 1. Remove or comment out USERNAME/PASSWORD in .env
|
||||
sed -i 's/^NEXTCLOUD_USERNAME/#NEXTCLOUD_USERNAME/' .env
|
||||
sed -i 's/^NEXTCLOUD_PASSWORD/#NEXTCLOUD_PASSWORD/' .env
|
||||
|
||||
# 2. Ensure NEXTCLOUD_HOST is set
|
||||
grep NEXTCLOUD_HOST .env
|
||||
|
||||
# 3. Restart server with OAuth
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
uv run nextcloud-mcp-server --oauth
|
||||
```
|
||||
|
||||
#### To switch from OAuth to BasicAuth:
|
||||
|
||||
```bash
|
||||
# 1. Add USERNAME/PASSWORD to .env
|
||||
echo "NEXTCLOUD_USERNAME=your-username" >> .env
|
||||
echo "NEXTCLOUD_PASSWORD=your-password" >> .env
|
||||
|
||||
# 2. Restart server (BasicAuth auto-detected, or use --no-oauth)
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
uv run nextcloud-mcp-server --no-oauth
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### For More OAuth Help
|
||||
|
||||
See the dedicated **[OAuth Troubleshooting Guide](oauth-troubleshooting.md)** for:
|
||||
- Bearer token authentication failures
|
||||
- PKCE validation errors
|
||||
- Token validation issues
|
||||
- Client registration problems
|
||||
- Advanced OAuth debugging
|
||||
- And much more...
|
||||
Restart the server after changing modes. The active mode is logged at startup; you can also set `MCP_DEPLOYMENT_MODE` explicitly to fail fast if the env vars don't match.
|
||||
|
||||
---
|
||||
|
||||
@@ -529,7 +310,7 @@ kill <pid>
|
||||
|
||||
### Issue: Server crashes or restarts frequently
|
||||
|
||||
**Cause:** Various issues including memory limits, uncaught exceptions, or OAuth token expiration.
|
||||
**Cause:** Various issues including memory limits or uncaught exceptions.
|
||||
|
||||
**Solution:**
|
||||
|
||||
@@ -553,38 +334,17 @@ kill <pid>
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. **Check for OAuth credential expiration** (if using dynamic registration):
|
||||
- See ["Stored client has expired"](#issue-stored-client-has-expired) above
|
||||
|
||||
---
|
||||
|
||||
## Connection Issues
|
||||
|
||||
### Issue: MCP client can't authenticate
|
||||
|
||||
**Cause:** OAuth flow failing or credentials invalid.
|
||||
**Cause:** Auth flow failing or credentials invalid.
|
||||
|
||||
**Solution:**
|
||||
|
||||
**For OAuth:**
|
||||
1. Verify OAuth is configured correctly:
|
||||
```bash
|
||||
uv run nextcloud-mcp-server --oauth --log-level debug
|
||||
# Look for "OAuth initialization complete"
|
||||
```
|
||||
|
||||
2. Check that OIDC app is accessible:
|
||||
```bash
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
3. Verify MCP_SERVER_URL matches your setup:
|
||||
```bash
|
||||
echo $NEXTCLOUD_MCP_SERVER_URL
|
||||
# Should match the URL clients use to connect
|
||||
```
|
||||
|
||||
**For BasicAuth:**
|
||||
**For BasicAuth modes:**
|
||||
1. Verify credentials work:
|
||||
```bash
|
||||
curl -u "$NEXTCLOUD_USERNAME:$NEXTCLOUD_PASSWORD" \
|
||||
@@ -592,6 +352,20 @@ kill <pid>
|
||||
-H "OCS-APIRequest: true"
|
||||
```
|
||||
|
||||
**For Login Flow v2 mode:**
|
||||
1. Verify the server starts the OAuth issuer:
|
||||
```bash
|
||||
uv run nextcloud-mcp-server --oauth --log-level debug
|
||||
# Look for "OAuth initialization complete"
|
||||
```
|
||||
|
||||
2. Verify `NEXTCLOUD_MCP_SERVER_URL` matches the URL clients use to connect:
|
||||
```bash
|
||||
echo $NEXTCLOUD_MCP_SERVER_URL
|
||||
```
|
||||
|
||||
3. See [Login Flow v2 troubleshooting](login-flow-v2.md#troubleshooting) for app-password and provisioning issues.
|
||||
|
||||
---
|
||||
|
||||
### Issue: Tools return errors or don't work
|
||||
@@ -610,15 +384,10 @@ kill <pid>
|
||||
- Ensure the authenticated user has access to the resources
|
||||
- Check sharing permissions for shared resources
|
||||
|
||||
3. **Test API directly:**
|
||||
3. **Test API directly with Basic Auth:**
|
||||
```bash
|
||||
# Test Notes API
|
||||
curl -u "$NEXTCLOUD_USERNAME:$NEXTCLOUD_PASSWORD" \
|
||||
"$NEXTCLOUD_HOST/apps/notes/api/v1/notes"
|
||||
|
||||
# Test with OAuth Bearer token
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"$NEXTCLOUD_HOST/apps/notes/api/v1/notes"
|
||||
```
|
||||
|
||||
4. **Check server logs for specific errors:**
|
||||
@@ -640,29 +409,20 @@ uv run nextcloud-mcp-server --log-level debug
|
||||
|
||||
Review the logs for specific error messages.
|
||||
|
||||
### 2. Verify OIDC Configuration (OAuth mode)
|
||||
### 2. Test Nextcloud Connectivity
|
||||
|
||||
```bash
|
||||
# Check OIDC discovery
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration
|
||||
# Verify Nextcloud is reachable from the MCP server
|
||||
curl -I "$NEXTCLOUD_HOST/status.php"
|
||||
|
||||
# Check registration endpoint exists
|
||||
curl https://your.nextcloud.instance.com/.well-known/openid-configuration | jq '.registration_endpoint'
|
||||
```
|
||||
|
||||
### 3. Test Nextcloud API Access
|
||||
|
||||
```bash
|
||||
# Test OCS API (should work with OAuth)
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
# With Basic Auth (Single-User or Multi-User BasicAuth modes)
|
||||
curl -u "$NEXTCLOUD_USERNAME:$NEXTCLOUD_PASSWORD" \
|
||||
"$NEXTCLOUD_HOST/ocs/v2.php/cloud/capabilities?format=json" \
|
||||
-H "OCS-APIRequest: true"
|
||||
|
||||
# Test app API (may need patch - see oauth2-bearer-token-session-issue.md)
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"$NEXTCLOUD_HOST/apps/notes/api/v1/notes"
|
||||
```
|
||||
|
||||
For Login Flow v2 mode, see [Login Flow v2 troubleshooting](login-flow-v2.md#troubleshooting).
|
||||
|
||||
### 4. Check Versions
|
||||
|
||||
```bash
|
||||
@@ -681,7 +441,7 @@ If problems persist, open an issue on the [GitHub repository](https://github.com
|
||||
|
||||
- **Server logs** (with `--log-level debug`)
|
||||
- **Nextcloud version**
|
||||
- **OIDC app version** (if using OAuth)
|
||||
- **Deployment mode** (single_user_basic / multi_user_basic / login_flow_v2)
|
||||
- **Error messages**
|
||||
- **Steps to reproduce**
|
||||
- **Environment details** (OS, Python version, Docker vs local)
|
||||
@@ -690,9 +450,7 @@ If problems persist, open an issue on the [GitHub repository](https://github.com
|
||||
|
||||
## See Also
|
||||
|
||||
- **[OAuth Troubleshooting](oauth-troubleshooting.md)** - Dedicated OAuth troubleshooting guide
|
||||
- [OAuth Setup Guide](oauth-setup.md) - OAuth configuration
|
||||
- [OAuth Architecture](oauth-architecture.md) - How OAuth works
|
||||
- [Upstream Status](oauth-upstream-status.md) - Required patches and upstream PRs
|
||||
- [Authentication](authentication.md) - Authentication modes
|
||||
- [Login Flow v2](login-flow-v2.md) - Multi-user setup, scope reference, troubleshooting FAQ
|
||||
- [Configuration](configuration.md) - Environment variables
|
||||
- [Running the Server](running.md) - Server options
|
||||
|
||||
Reference in New Issue
Block a user