Skip to content

Commit f37008f

Browse files
authored
Merge pull request #254 from cbcoutinho/feature/keycloak
feat: Complete Keycloak external IdP integration with ADR-002 implementation
2 parents eb8ca92 + 7cb616c commit f37008f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+8170
-1575
lines changed

.env.keycloak.sample

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Keycloak OAuth Configuration for Nextcloud MCP Server
2+
#
3+
# This configuration uses Keycloak as the OAuth/OIDC identity provider
4+
# while still accessing Nextcloud APIs. Nextcloud's user_oidc app validates
5+
# Keycloak bearer tokens and provisions users automatically.
6+
#
7+
# Architecture: Client → Keycloak (OAuth) → MCP Server → Nextcloud (user_oidc validates) → APIs
8+
#
9+
# This enables ADR-002 authentication patterns without admin credentials!
10+
11+
# ==============================================================================
12+
# OAUTH PROVIDER SELECTION
13+
# ==============================================================================
14+
15+
# OAuth provider: "keycloak" or "nextcloud" (default)
16+
OAUTH_PROVIDER=keycloak
17+
18+
# ==============================================================================
19+
# KEYCLOAK CONFIGURATION
20+
# ==============================================================================
21+
22+
# Keycloak base URL (accessible from MCP server container)
23+
KEYCLOAK_URL=http://keycloak:8080
24+
25+
# Keycloak realm name
26+
KEYCLOAK_REALM=nextcloud-mcp
27+
28+
# OAuth client credentials (from Keycloak realm export or manual configuration)
29+
KEYCLOAK_CLIENT_ID=nextcloud-mcp-server
30+
KEYCLOAK_CLIENT_SECRET=mcp-secret-change-in-production
31+
32+
# OIDC discovery URL (auto-constructed from URL + realm, or specify explicitly)
33+
KEYCLOAK_DISCOVERY_URL=http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
34+
35+
# ==============================================================================
36+
# NEXTCLOUD CONFIGURATION
37+
# ==============================================================================
38+
39+
# Nextcloud URL (accessible from MCP server container)
40+
# Used for API access - Keycloak tokens are validated by user_oidc app
41+
NEXTCLOUD_HOST=http://app:80
42+
43+
# MCP server URL (for OAuth redirect URIs)
44+
# This is the publicly accessible URL that OAuth clients connect to
45+
NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
46+
47+
# Public Keycloak issuer URL (accessible from OAuth clients)
48+
# If clients access Keycloak via a different URL than the internal one,
49+
# set this to the public URL for OAuth flows
50+
NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8888
51+
52+
# ==============================================================================
53+
# REFRESH TOKEN STORAGE (ADR-002 Tier 1: Offline Access)
54+
# ==============================================================================
55+
56+
# Enable offline_access scope to get refresh tokens
57+
ENABLE_OFFLINE_ACCESS=true
58+
59+
# Encryption key for storing refresh tokens (generate with instructions below)
60+
# IMPORTANT: Keep this secret! Tokens are encrypted at rest using this key.
61+
#
62+
# Generate a key:
63+
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
64+
#
65+
# Example (DO NOT use this in production!):
66+
# TOKEN_ENCRYPTION_KEY=your-base64-encoded-fernet-key-here
67+
68+
# Path to SQLite database for token storage
69+
TOKEN_STORAGE_DB=/app/data/tokens.db
70+
71+
# ==============================================================================
72+
# DOCKER COMPOSE NOTES
73+
# ==============================================================================
74+
75+
# When running via docker-compose, the mcp-keycloak service is pre-configured
76+
# with these environment variables. See docker-compose.yml for the full config.
77+
#
78+
# Start services:
79+
# docker-compose up -d keycloak app mcp-keycloak
80+
#
81+
# View logs:
82+
# docker-compose logs -f mcp-keycloak
83+
#
84+
# Check Keycloak realm:
85+
# curl http://localhost:8888/realms/nextcloud-mcp/.well-known/openid-configuration
86+
#
87+
# Check user_oidc provider:
88+
# docker compose exec app php occ user_oidc:provider keycloak
89+
90+
# ==============================================================================
91+
# KEYCLOAK SETUP VERIFICATION
92+
# ==============================================================================
93+
94+
# 1. Verify Keycloak is running and realm is imported:
95+
# curl http://localhost:8888/realms/nextcloud-mcp/.well-known/openid-configuration
96+
#
97+
# 2. Verify Nextcloud user_oidc provider is configured:
98+
# docker compose exec app php occ user_oidc:provider keycloak
99+
#
100+
# 3. Test OAuth flow manually:
101+
# - Get token from Keycloak:
102+
# curl -X POST "http://localhost:8888/realms/nextcloud-mcp/protocol/openid-connect/token" \
103+
# -d "grant_type=password" \
104+
# -d "client_id=nextcloud-mcp-server" \
105+
# -d "client_secret=mcp-secret-change-in-production" \
106+
# -d "username=admin" \
107+
# -d "password=admin" \
108+
# -d "scope=openid profile email offline_access"
109+
#
110+
# - Use token with Nextcloud API:
111+
# curl -H "Authorization: Bearer <access_token>" \
112+
# http://localhost:8080/ocs/v2.php/cloud/capabilities
113+
#
114+
# 4. Connect MCP client to server:
115+
# - Point your MCP client to http://localhost:8002
116+
# - Complete OAuth flow via Keycloak (credentials: admin/admin)
117+
# - Client should receive access token and be able to call MCP tools
118+
119+
# ==============================================================================
120+
# TROUBLESHOOTING
121+
# ==============================================================================
122+
123+
# If OAuth flow fails:
124+
# - Check that Keycloak is accessible: curl http://localhost:8888
125+
# - Check that user_oidc provider is configured: docker compose exec app php occ user_oidc:provider keycloak
126+
# - Check MCP server logs: docker-compose logs mcp-keycloak
127+
# - Verify redirect URIs match in Keycloak client configuration
128+
#
129+
# If token validation fails:
130+
# - Verify user_oidc has bearer validation enabled (--check-bearer=1)
131+
# - Check Nextcloud logs: docker compose exec app tail -f /var/www/html/data/nextcloud.log
132+
# - Verify Keycloak discovery URL is accessible from Nextcloud container:
133+
# docker compose exec app curl http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
134+
#
135+
# If offline_access/refresh tokens not working:
136+
# - Verify TOKEN_ENCRYPTION_KEY is set and valid
137+
# - Check token storage database: ls -lah /app/data/tokens.db (inside container)
138+
# - Check that offline_access scope is requested in realm configuration

CLAUDE.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,137 @@ uv run pytest -m oauth -v
395395
- Playwright tests run in CI/CD environments
396396
- Use Firefox browser in CI: `--browser firefox` (Chromium may have issues with localhost redirects)
397397

398+
#### Keycloak OAuth/OIDC Testing (ADR-002 Integration)
399+
400+
The MCP server supports using **Keycloak as an external OAuth/OIDC identity provider** instead of Nextcloud's built-in OIDC app. This validates the ADR-002 architecture for background jobs and external identity providers.
401+
402+
**Architecture:**
403+
```
404+
MCP Client → Keycloak (OAuth) → MCP Server → Nextcloud user_oidc (validates token) → APIs
405+
```
406+
407+
**Key Benefits:**
408+
-**No admin credentials needed** - All API access uses user's Keycloak token
409+
-**External identity provider** - Demonstrates integration with enterprise IdPs
410+
-**ADR-002 validation** - Tests offline_access and refresh token patterns
411+
-**User provisioning** - Nextcloud automatically provisions users from Keycloak
412+
413+
**Setup and Testing:**
414+
```bash
415+
# 1. Start Keycloak and MCP server with Keycloak OAuth
416+
docker-compose up -d keycloak app mcp-keycloak
417+
418+
# 2. Verify Keycloak realm is available
419+
curl http://localhost:8888/realms/nextcloud-mcp/.well-known/openid-configuration
420+
421+
# 3. Verify user_oidc provider is configured
422+
docker compose exec app php occ user_oidc:provider keycloak
423+
424+
# 4. Generate encryption key for refresh token storage (optional, for offline access)
425+
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
426+
# Set in environment: export TOKEN_ENCRYPTION_KEY='<key>'
427+
428+
# 5. Test OAuth flow manually
429+
# Get token from Keycloak:
430+
TOKEN=$(curl -s -X POST "http://localhost:8888/realms/nextcloud-mcp/protocol/openid-connect/token" \
431+
-d "grant_type=password" \
432+
-d "client_id=mcp-client" \
433+
-d "client_secret=mcp-secret-change-in-production" \
434+
-d "username=admin" \
435+
-d "password=admin" \
436+
-d "scope=openid profile email offline_access" | jq -r .access_token)
437+
438+
# Use token with Nextcloud API (validated by user_oidc):
439+
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/ocs/v2.php/cloud/capabilities
440+
441+
# 6. Connect MCP client
442+
# Point client to: http://localhost:8002
443+
# Complete OAuth flow using Keycloak credentials: admin/admin
444+
```
445+
446+
**Three MCP Server Containers:**
447+
- **`mcp`** (port 8000): Basic auth with admin credentials
448+
- **`mcp-oauth`** (port 8001): Nextcloud OIDC provider (JWT tokens)
449+
- **`mcp-keycloak`** (port 8002): Keycloak OIDC provider (external IdP)
450+
451+
**Keycloak Configuration:**
452+
- **Realm**: `nextcloud-mcp` (auto-imported from `keycloak/realm-export.json`)
453+
- **Client**: `mcp-client` (pre-configured with PKCE, offline_access)
454+
- **Admin user**: `admin/admin` (created in realm export)
455+
- **Redirect URIs**: `http://localhost:*/callback`, `http://127.0.0.1:*/callback`
456+
457+
**Environment Variables** (Generic OIDC - works with any provider):
458+
```bash
459+
# Generic OIDC configuration (provider-agnostic)
460+
OIDC_DISCOVERY_URL=http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
461+
OIDC_CLIENT_ID=nextcloud-mcp-server # OAuth client ID
462+
OIDC_CLIENT_SECRET=mcp-secret-... # OAuth client secret
463+
464+
# Nextcloud API configuration
465+
NEXTCLOUD_HOST=http://app:80 # Nextcloud API (token validation in external IdP mode)
466+
467+
# Refresh tokens and token exchange (ADR-002)
468+
ENABLE_OFFLINE_ACCESS=true # Enable refresh tokens
469+
TOKEN_ENCRYPTION_KEY=<fernet-key> # Encrypt refresh tokens
470+
TOKEN_STORAGE_DB=/app/data/tokens.db # Token storage path
471+
472+
# OAuth scopes (optional - uses defaults if not specified)
473+
NEXTCLOUD_OIDC_SCOPES=openid profile email offline_access notes:read notes:write ...
474+
```
475+
476+
**Provider Mode Detection:**
477+
- **External IdP mode**: If `OIDC_DISCOVERY_URL` issuer ≠ `NEXTCLOUD_HOST` → Uses external provider (Keycloak, Auth0, Okta, etc.)
478+
- **Integrated mode**: If `OIDC_DISCOVERY_URL` not set or issuer = `NEXTCLOUD_HOST` → Uses Nextcloud OIDC app
479+
480+
**Nextcloud user_oidc Configuration:**
481+
The `user_oidc` app is automatically configured by `app-hooks/post-installation/15-setup-keycloak-provider.sh`:
482+
```bash
483+
# Configured with:
484+
--check-bearer=1 # Validate bearer tokens
485+
--bearer-provisioning=1 # Auto-provision users
486+
--unique-uid=1 # Hash user IDs
487+
--scope="openid profile email offline_access"
488+
```
489+
490+
**Troubleshooting:**
491+
```bash
492+
# Check Keycloak is running
493+
docker-compose ps keycloak
494+
docker-compose logs keycloak
495+
496+
# Check user_oidc provider configuration
497+
docker compose exec app php occ user_oidc:provider keycloak
498+
499+
# Check MCP server logs
500+
docker-compose logs -f mcp-keycloak
501+
502+
# Check Nextcloud logs for token validation
503+
docker compose exec app tail -f /var/www/html/data/nextcloud.log
504+
505+
# Verify Keycloak is accessible from Nextcloud container
506+
docker compose exec app curl http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
507+
```
508+
509+
**ADR-002 Offline Access Testing:**
510+
The Keycloak integration enables testing ADR-002's primary authentication pattern (offline access with refresh tokens):
511+
512+
1. **Refresh token storage**: Tokens stored encrypted in SQLite (`/app/data/tokens.db`)
513+
2. **Token refresh**: Access tokens refreshed automatically when expired
514+
3. **Background workers**: Can access APIs using stored refresh tokens
515+
4. **No admin credentials**: All operations use user's OAuth tokens
516+
517+
**Note**: Service account tokens (client_credentials grant) were considered but rejected as they create Nextcloud user accounts and violate OAuth "act on-behalf-of" principles. See ADR-002 "Will Not Implement" section.
518+
519+
See `docs/ADR-002-vector-sync-authentication.md` for architectural details.
520+
521+
**Audience Validation:**
522+
Tokens include `aud: ["mcp-server", "nextcloud"]` claims for proper security:
523+
- MCP server validates tokens are intended for it
524+
- Nextcloud validates tokens include it as audience
525+
- Prevents token misuse across services
526+
527+
See `docs/audience-validation-setup.md` for configuration details and `docs/keycloak-multi-client-validation.md` for realm-level validation behavior.
528+
398529
### Configuration Files
399530

400531
- **`pyproject.toml`** - Python project configuration using uv for dependency management
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
2+
index 4453f5a7d4b..f1ca9b48d21 100644
3+
--- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
4+
+++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
5+
@@ -73,6 +73,13 @@ class CORSMiddleware extends Middleware {
6+
$user = array_key_exists('PHP_AUTH_USER', $this->request->server) ? $this->request->server['PHP_AUTH_USER'] : null;
7+
$pass = array_key_exists('PHP_AUTH_PW', $this->request->server) ? $this->request->server['PHP_AUTH_PW'] : null;
8+
9+
+ // Allow Bearer token authentication for CORS requests
10+
+ // Bearer tokens are stateless and don't require CSRF protection
11+
+ $authorizationHeader = $this->request->getHeader('Authorization');
12+
+ if (!empty($authorizationHeader) && str_starts_with($authorizationHeader, 'Bearer ')) {
13+
+ return;
14+
+ }
15+
+
16+
// Allow to use the current session if a CSRF token is provided
17+
if ($this->request->passesCSRFCheck()) {
18+
return;

app-hooks/post-installation/10-install-user_oidc-app.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,13 @@ php /var/www/html/occ app:enable user_oidc
99

1010
# Configure user_oidc to validate bearer tokens from the OIDC Identity Provider
1111
php /var/www/html/occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean
12+
php /var/www/html/occ config:system:set user_oidc httpclient.allowselfsigned --value=true --type=boolean
1213

13-
patch -u /var/www/html/custom_apps/user_oidc/lib/User/Backend.php -i /docker-entrypoint-hooks.d/post-installation/0001-Fix-Bearer-token-authentication-causing-session-logo.patch
14+
# Allow Nextcloud to connect to local/internal servers (required for external IdP mode)
15+
# This enables user_oidc to fetch JWKS from internal Keycloak container
16+
php /var/www/html/occ config:system:set allow_local_remote_servers --value=true --type=boolean
17+
18+
# Note: The user_oidc app_api session flag patch is NOT required when using the
19+
# CORSMiddleware Bearer token patch (20-apply-cors-bearer-token-patch.sh).
20+
# The CORSMiddleware patch fixes the root cause by allowing Bearer tokens to bypass
21+
# CORS/CSRF checks at the framework level.

0 commit comments

Comments
 (0)