docs: Update jwt docs [skip ci]
This commit is contained in:
@@ -1,100 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=== JWT OAuth Client Setup ==="
|
|
||||||
echo "Installing and configuring OIDC app for JWT tokens..."
|
|
||||||
|
|
||||||
# Wait for Nextcloud to be fully initialized
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# Install OIDC app if not already installed
|
|
||||||
if ! php /var/www/html/occ app:list | grep -q "oidc"; then
|
|
||||||
echo "Installing OIDC app..."
|
|
||||||
php /var/www/html/occ app:install oidc
|
|
||||||
else
|
|
||||||
echo "OIDC app already installed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Enable the app
|
|
||||||
php /var/www/html/occ app:enable oidc
|
|
||||||
|
|
||||||
# Check if JWT client already exists
|
|
||||||
# Use a location that www-data user owns
|
|
||||||
CLIENT_DIR="/var/www/html/.oauth-jwt"
|
|
||||||
CLIENT_FILE="$CLIENT_DIR/nextcloud_oauth_client.json"
|
|
||||||
|
|
||||||
if [ -f "$CLIENT_FILE" ]; then
|
|
||||||
echo "JWT OAuth client already exists at $CLIENT_FILE"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create directory owned by www-data
|
|
||||||
mkdir -p "$CLIENT_DIR"
|
|
||||||
|
|
||||||
# Create JWT OAuth client with proper scopes
|
|
||||||
echo "Creating JWT OAuth client..."
|
|
||||||
|
|
||||||
# The redirect URI for the MCP server
|
|
||||||
REDIRECT_URI="http://127.0.0.1:8002/oauth/callback"
|
|
||||||
|
|
||||||
# Create the client with JWT token type
|
|
||||||
OUTPUT=$(php /var/www/html/occ oidc:create \
|
|
||||||
--token_type=jwt \
|
|
||||||
--allowed_scopes="openid profile email nc:read nc:write" \
|
|
||||||
"Nextcloud MCP Server JWT" \
|
|
||||||
"$REDIRECT_URI")
|
|
||||||
|
|
||||||
echo "Client creation output:"
|
|
||||||
echo "$OUTPUT"
|
|
||||||
|
|
||||||
# Parse the JSON output to extract client_id, client_secret, and issued_at
|
|
||||||
# Output format is JSON
|
|
||||||
CLIENT_ID=$(echo "$OUTPUT" | grep '"client_id"' | sed 's/.*"client_id": "\([^"]*\)".*/\1/')
|
|
||||||
CLIENT_SECRET=$(echo "$OUTPUT" | grep '"client_secret"' | sed 's/.*"client_secret": "\([^"]*\)".*/\1/')
|
|
||||||
ISSUED_AT=$(echo "$OUTPUT" | grep '"issued_at"' | sed 's/.*"issued_at": \([0-9]*\).*/\1/')
|
|
||||||
|
|
||||||
if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then
|
|
||||||
echo "ERROR: Failed to parse client credentials from output"
|
|
||||||
echo "Output was: $OUTPUT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use issued_at if available, otherwise use current timestamp
|
|
||||||
if [ -z "$ISSUED_AT" ]; then
|
|
||||||
ISSUED_AT=$(date +%s)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set expiration to 10 years in the future (JWT clients don't expire like DCR clients)
|
|
||||||
EXPIRES_AT=$((ISSUED_AT + 315360000))
|
|
||||||
|
|
||||||
echo "Successfully created JWT client:"
|
|
||||||
echo " Client ID: ${CLIENT_ID:0:16}..."
|
|
||||||
echo " Client Secret: [hidden]"
|
|
||||||
|
|
||||||
# Create the credentials file in the format expected by the MCP server
|
|
||||||
# This matches the format from ClientInfo.to_dict() in client_registration.py
|
|
||||||
cat > "$CLIENT_FILE" << EOF
|
|
||||||
{
|
|
||||||
"client_id": "$CLIENT_ID",
|
|
||||||
"client_secret": "$CLIENT_SECRET",
|
|
||||||
"client_id_issued_at": $ISSUED_AT,
|
|
||||||
"client_secret_expires_at": $EXPIRES_AT,
|
|
||||||
"redirect_uris": ["$REDIRECT_URI"]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Credentials saved to $CLIENT_FILE"
|
|
||||||
|
|
||||||
# Also save to environment variable format for easy access
|
|
||||||
cat > "$CLIENT_DIR/client_env.sh" << EOF
|
|
||||||
export NEXTCLOUD_OIDC_CLIENT_ID="$CLIENT_ID"
|
|
||||||
export NEXTCLOUD_OIDC_CLIENT_SECRET="$CLIENT_SECRET"
|
|
||||||
export NEXTCLOUD_OIDC_SCOPES="openid profile email nc:read nc:write"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 600 "$CLIENT_DIR/client_env.sh"
|
|
||||||
|
|
||||||
echo "=== JWT OAuth Client Setup Complete ==="
|
|
||||||
echo "Client credentials are available at:"
|
|
||||||
echo " JSON: $CLIENT_FILE"
|
|
||||||
echo " ENV: $CLIENT_DIR/client_env.sh"
|
|
||||||
+204
-66
@@ -27,13 +27,12 @@ The Nextcloud MCP Server supports OAuth authentication with both **JWT** (RFC 90
|
|||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
|
|
||||||
✅ **JWT Token Support** - RFC 9068 compliant access tokens with RS256 signatures
|
- ✅ **JWT Token Support** - RFC 9068 compliant access tokens with RS256 signatures
|
||||||
✅ **Custom Scopes** - `nc:read` and `nc:write` for read/write access control
|
- ✅ **Custom Scopes** - `nc:read` and `nc:write` for read/write access control
|
||||||
✅ **Dynamic Tool Filtering** - Tools filtered based on user's token scopes
|
- ✅ **Dynamic Tool Filtering** - Tools filtered based on user's token scopes
|
||||||
✅ **Automatic Client Creation** - JWT OAuth clients auto-generated on container startup
|
- ✅ **Scope Challenges** - RFC-compliant `WWW-Authenticate` headers for insufficient scopes
|
||||||
✅ **Scope Challenges** - RFC-compliant `WWW-Authenticate` headers for insufficient scopes
|
- ✅ **Protected Resource Metadata** - RFC 8959 endpoint for scope discovery
|
||||||
✅ **Protected Resource Metadata** - RFC 8959 endpoint for scope discovery
|
- ✅ **Backward Compatible** - BasicAuth mode bypasses all scope checks
|
||||||
✅ **Backward Compatible** - BasicAuth mode bypasses all scope checks
|
|
||||||
|
|
||||||
### Supported Scopes
|
### Supported Scopes
|
||||||
|
|
||||||
@@ -88,14 +87,23 @@ The Nextcloud OIDC app supports two token formats, configured per-client:
|
|||||||
**Advantages:**
|
**Advantages:**
|
||||||
- ✅ Smaller size (72 characters)
|
- ✅ Smaller size (72 characters)
|
||||||
- ✅ No payload visible to client
|
- ✅ No payload visible to client
|
||||||
|
- ✅ Direct scope access via introspection endpoint (RFC 7662)
|
||||||
|
|
||||||
**Disadvantages:**
|
**Disadvantages:**
|
||||||
- ❌ Requires userinfo endpoint call for each validation
|
- ❌ Higher latency - Requires HTTP call to introspection endpoint
|
||||||
- ❌ Scopes must be inferred from userinfo response (not directly available)
|
- ❌ Slower than JWT signature verification (network roundtrip)
|
||||||
- ❌ Higher latency (HTTP call required)
|
|
||||||
|
**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:**
|
**When to Use:**
|
||||||
- Use **JWT tokens** for production (better performance, direct scope access)
|
- Use **JWT tokens** for production (better performance, no HTTP call)
|
||||||
- Use **opaque tokens** for compatibility with clients that don't support JWT
|
- Use **opaque tokens** for compatibility with clients that don't support JWT
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -228,8 +236,9 @@ The development environment includes three MCP server variants:
|
|||||||
|
|
||||||
### JWT Service Configuration
|
### JWT Service Configuration
|
||||||
|
|
||||||
The `mcp-oauth-jwt` service uses automatic client creation:
|
The `mcp-oauth-jwt` service uses **Dynamic Client Registration (DCR)** by default:
|
||||||
|
|
||||||
|
**Default Configuration (DCR):**
|
||||||
```yaml
|
```yaml
|
||||||
mcp-oauth-jwt:
|
mcp-oauth-jwt:
|
||||||
build: .
|
build: .
|
||||||
@@ -240,59 +249,150 @@ mcp-oauth-jwt:
|
|||||||
- NEXTCLOUD_HOST=http://app:80
|
- NEXTCLOUD_HOST=http://app:80
|
||||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
|
||||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||||
- NEXTCLOUD_OIDC_CLIENT_STORAGE=/var/www/html/.oauth-jwt/nextcloud_oauth_client.json
|
|
||||||
- NEXTCLOUD_OIDC_SCOPES=openid profile email nc:read nc:write
|
- NEXTCLOUD_OIDC_SCOPES=openid profile email nc:read nc:write
|
||||||
- NEXTCLOUD_OIDC_TOKEN_TYPE=jwt
|
- NEXTCLOUD_OIDC_TOKEN_TYPE=jwt
|
||||||
volumes:
|
volumes:
|
||||||
- nextcloud:/var/www/html:ro
|
- ./oauth-storage:/app/.oauth # Optional: persist DCR credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Pre-Configured Credentials:**
|
||||||
|
```yaml
|
||||||
|
mcp-oauth-jwt:
|
||||||
|
build: .
|
||||||
|
command: ["--transport", "streamable-http", "--oauth", "--port", "8002"]
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:8002:8002
|
||||||
|
environment:
|
||||||
|
- NEXTCLOUD_HOST=http://app:80
|
||||||
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
|
||||||
|
- 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
|
||||||
|
- NEXTCLOUD_OIDC_TOKEN_TYPE=jwt
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Points:**
|
**Key Points:**
|
||||||
- No `NEXTCLOUD_OIDC_CLIENT_ID` or `CLIENT_SECRET` needed (loaded from storage)
|
- **No credentials needed** - DCR automatically registers the client on first start
|
||||||
- JWT client auto-created during Nextcloud initialization
|
- **Credentials persist** - Saved to `.nextcloud_oauth_client.json` and reused
|
||||||
- Credentials stored in `/var/www/html/.oauth-jwt/nextcloud_oauth_client.json`
|
- **JWT tokens** - Set `TOKEN_TYPE=jwt` for better performance
|
||||||
- Volume mounted read-only for security
|
- **Pre-configured credentials** - Providing `CLIENT_ID`/`CLIENT_SECRET` skips DCR
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `NEXTCLOUD_HOST` | Nextcloud base URL | `http://localhost:8080` |
|
| `NEXTCLOUD_HOST` | Nextcloud base URL | `http://localhost:8080` |
|
||||||
| `NEXTCLOUD_MCP_SERVER_URL` | MCP server external URL | (required) |
|
| `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_PUBLIC_ISSUER_URL` | Public issuer URL for JWT validation | (uses `NEXTCLOUD_HOST`) |
|
||||||
| `NEXTCLOUD_OIDC_CLIENT_STORAGE` | Path to client credentials JSON | (optional - uses DCR if unset) |
|
| `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_CLIENT_STORAGE` | Path to persist DCR-registered credentials | `.nextcloud_oauth_client.json` |
|
||||||
| `NEXTCLOUD_OIDC_SCOPES` | Space-separated scopes to request | `"openid profile email nc:read nc:write"` |
|
| `NEXTCLOUD_OIDC_SCOPES` | Space-separated scopes to request | `"openid profile email nc:read nc:write"` |
|
||||||
| `NEXTCLOUD_OIDC_TOKEN_TYPE` | Token format: `"jwt"` or `"Bearer"` | `"Bearer"` |
|
| `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. Storage File (Second Priority)
|
||||||
|
└─ NEXTCLOUD_OIDC_CLIENT_STORAGE (.nextcloud_oauth_client.json)
|
||||||
|
|
||||||
|
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 nc:read nc:write"
|
||||||
|
export NEXTCLOUD_OIDC_TOKEN_TYPE=jwt # or "Bearer" for opaque tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
**Credential Storage:**
|
||||||
|
|
||||||
|
- Registered credentials are saved to `NEXTCLOUD_OIDC_CLIENT_STORAGE` (default: `.nextcloud_oauth_client.json`)
|
||||||
|
- File has restrictive permissions (0600 - owner read/write only)
|
||||||
|
- Credentials are reused on subsequent starts (no re-registration needed)
|
||||||
|
- Storage file is 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
|
||||||
|
|
||||||
If not using automatic creation, create a JWT client manually:
|
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
|
```bash
|
||||||
docker compose exec app php occ oidc:create \
|
docker compose exec app php occ oidc:create \
|
||||||
--token_type=jwt \
|
--token_type=jwt \
|
||||||
--allowed_scopes="openid profile email nc:read nc:write" \
|
--allowed_scopes="openid profile email nc:read nc:write" \
|
||||||
"Nextcloud MCP Server" \
|
"Nextcloud MCP Server" \
|
||||||
"http://localhost:8002/oauth/callback"
|
"http://localhost:8000/oauth/callback"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output:**
|
**Output:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"client_id": "...",
|
"client_id": "XBd2xqIisu3Kswg39Ub4BUhC36PEYjwwivx3G5nZdDgigvwKXrTHozs7m9DeoLSY",
|
||||||
"client_secret": "...",
|
"client_secret": "xNKcy0qpUSau36T60pGGdb03pMEVLXtqykxjK8YkDpoNxNcZ4ClyAT3IAEse2AKT",
|
||||||
"token_type": "jwt",
|
"token_type": "jwt",
|
||||||
"allowed_scopes": "openid profile email nc:read nc:write"
|
"allowed_scopes": "openid profile email nc:read nc:write"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then configure the MCP server:
|
**Configure MCP Server with Pre-Configured Credentials:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Option 1: Environment variables (highest priority)
|
||||||
export NEXTCLOUD_OIDC_CLIENT_ID="<client_id>"
|
export NEXTCLOUD_OIDC_CLIENT_ID="<client_id>"
|
||||||
export NEXTCLOUD_OIDC_CLIENT_SECRET="<client_secret>"
|
export NEXTCLOUD_OIDC_CLIENT_SECRET="<client_secret>"
|
||||||
export NEXTCLOUD_OIDC_TOKEN_TYPE="jwt"
|
export NEXTCLOUD_OIDC_TOKEN_TYPE="jwt"
|
||||||
|
|
||||||
|
# Option 2: Storage file (second priority)
|
||||||
|
# Save the JSON response to .nextcloud_oauth_client.json
|
||||||
|
# Server will automatically load it on startup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When credentials are provided via environment variables or storage file, **DCR is skipped**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -317,8 +417,8 @@ export NEXTCLOUD_OIDC_TOKEN_TYPE="jwt"
|
|||||||
│ ┌───────────────────────────────────────────────────┐ │
|
│ ┌───────────────────────────────────────────────────┐ │
|
||||||
│ │ NextcloudTokenVerifier │ │
|
│ │ NextcloudTokenVerifier │ │
|
||||||
│ │ - JWT signature verification (JWKS) │ │
|
│ │ - JWT signature verification (JWKS) │ │
|
||||||
│ │ - Scope extraction from token payload │ │
|
│ │ - Introspection endpoint (opaque tokens) │ │
|
||||||
│ │ - Fallback to userinfo endpoint (opaque tokens) │ │
|
│ │ - Userinfo fallback (last resort) │ │
|
||||||
│ └───────────────────┬───────────────────────────────┘ │
|
│ └───────────────────┬───────────────────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ v │
|
│ v │
|
||||||
@@ -342,10 +442,13 @@ export NEXTCLOUD_OIDC_TOKEN_TYPE="jwt"
|
|||||||
### Key Components
|
### Key Components
|
||||||
|
|
||||||
**1. Token Verification** (`nextcloud_mcp_server/auth/token_verifier.py`)
|
**1. Token Verification** (`nextcloud_mcp_server/auth/token_verifier.py`)
|
||||||
- JWT signature verification using JWKS (RS256)
|
- **Three-tier validation strategy:**
|
||||||
- Scope extraction from `scope` claim
|
1. **JWT verification** (lines 116-124): JWKS signature validation for JWT tokens
|
||||||
- Token caching with TTL
|
2. **Introspection** (lines 126-134): RFC 7662 endpoint for opaque tokens
|
||||||
- Fallback to userinfo 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`)
|
**2. Scope Authorization** (`nextcloud_mcp_server/auth/scope_authorization.py`)
|
||||||
- `@require_scopes()` decorator for tools
|
- `@require_scopes()` decorator for tools
|
||||||
@@ -369,28 +472,66 @@ export NEXTCLOUD_OIDC_TOKEN_TYPE="jwt"
|
|||||||
- Returns 403 with `WWW-Authenticate` header
|
- Returns 403 with `WWW-Authenticate` header
|
||||||
- Includes missing scopes and PRM endpoint URL
|
- Includes missing scopes and PRM endpoint URL
|
||||||
|
|
||||||
### Automatic Client Creation
|
### Token Validation Flow
|
||||||
|
|
||||||
**Post-Installation Hook:** `app-hooks/post-installation/90-create-jwt-oauth-client.sh`
|
The `NextcloudTokenVerifier` implements a **cascading validation strategy** that handles both JWT and opaque tokens efficiently:
|
||||||
|
|
||||||
On container startup, this script:
|
```
|
||||||
1. Installs/enables OIDC app
|
┌─────────────────────────────────────────────────────────┐
|
||||||
2. Creates JWT client via `occ oidc:create --token_type=jwt`
|
│ verify_token(token) │
|
||||||
3. Parses JSON output for credentials
|
│ (nextcloud_mcp_server/auth/token_verifier.py:88-142) │
|
||||||
4. Saves to `/var/www/html/.oauth-jwt/nextcloud_oauth_client.json`
|
└────────────────────────┬────────────────────────────────┘
|
||||||
|
│
|
||||||
**Credential Format:**
|
├──> 1. Check cache (lines 106-109)
|
||||||
```json
|
│ ├─ Hit: Return cached AccessToken
|
||||||
{
|
│ └─ Miss: Continue to validation
|
||||||
"client_id": "XBd2xqIisu3Kswg39Ub4BUhC36PEYjwwivx3G5nZdDgigvwKXrTHozs7m9DeoLSY",
|
│
|
||||||
"client_secret": "xNKcy0qpUSau36T60pGGdb03pMEVLXtqykxjK8YkDpoNxNcZ4ClyAT3IAEse2AKT",
|
├──> 2. JWT Format Check (lines 112-124)
|
||||||
"client_id_issued_at": 1761097039,
|
│ ├─ Token has 3 parts (header.payload.signature)?
|
||||||
"client_secret_expires_at": 2076457039,
|
│ │ └─ Yes: Attempt JWT verification
|
||||||
"redirect_uris": ["http://localhost:8002/oauth/callback"]
|
│ │ ├─ 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 nc:read nc: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
|
## Testing
|
||||||
|
|
||||||
@@ -565,26 +706,26 @@ docker compose exec mcp-oauth-jwt env | grep NEXTCLOUD_OIDC
|
|||||||
# Ensure no NEXTCLOUD_USERNAME or NEXTCLOUD_PASSWORD set
|
# Ensure no NEXTCLOUD_USERNAME or NEXTCLOUD_PASSWORD set
|
||||||
```
|
```
|
||||||
|
|
||||||
### Issue: Dynamic Client Registration Doesn't Set Scopes
|
### Verifying DCR Scope Configuration
|
||||||
|
|
||||||
**Symptom:** Client registered via DCR has empty `allowed_scopes`
|
DCR **now properly sets `allowed_scopes`** when the `scope` parameter is provided during registration.
|
||||||
|
|
||||||
**Cause:** OIDC app's registration endpoint doesn't populate `allowed_scopes` from request
|
**To verify DCR scopes are working:**
|
||||||
|
|
||||||
**Workaround:** Use pre-configured clients instead of DCR for JWT tokens:
|
|
||||||
```bash
|
```bash
|
||||||
# Create client manually via occ
|
# Check the registered client's allowed_scopes via database
|
||||||
docker compose exec app php occ oidc:create \
|
docker compose exec db mariadb -u nextcloud -ppassword nextcloud \
|
||||||
--token_type=jwt \
|
-e "SELECT name, allowed_scopes FROM oc_oauth2_clients WHERE name LIKE 'DCR-%' ORDER BY id DESC LIMIT 1;"
|
||||||
--allowed_scopes="openid profile email nc:read nc:write" \
|
|
||||||
"Client Name" \
|
|
||||||
"http://callback"
|
|
||||||
|
|
||||||
# Configure MCP server to use these credentials
|
# Should show your requested scopes (e.g., "openid profile email nc:read nc:write")
|
||||||
export NEXTCLOUD_OIDC_CLIENT_ID="..."
|
|
||||||
export NEXTCLOUD_OIDC_CLIENT_SECRET="..."
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**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. Delete `.nextcloud_oauth_client.json` and restart to force re-registration
|
||||||
|
|
||||||
### Issue: Token Type Case Sensitivity
|
### Issue: Token Type Case Sensitivity
|
||||||
|
|
||||||
**Symptom:** JWT tokens not generated even though `token_type=JWT` set
|
**Symptom:** JWT tokens not generated even though `token_type=JWT` set
|
||||||
@@ -716,9 +857,7 @@ WARNING Missing required scopes: nc:write
|
|||||||
### Known Limitations
|
### Known Limitations
|
||||||
|
|
||||||
1. **No Fine-Grained Scopes** - Only coarse `nc:read` and `nc:write` (not per-app scopes)
|
1. **No Fine-Grained Scopes** - Only coarse `nc:read` and `nc:write` (not per-app scopes)
|
||||||
2. **DCR Doesn't Set Allowed Scopes** - Must use pre-configured clients for JWT
|
2. **No Refresh Token Support** - Tokens must be reacquired when expired
|
||||||
3. **No Introspection Endpoint** - OIDC app lacks RFC 7662 introspection
|
|
||||||
4. **No Refresh Token Support** - Tokens must be reacquired when expired
|
|
||||||
|
|
||||||
### Future Enhancements
|
### Future Enhancements
|
||||||
|
|
||||||
@@ -727,7 +866,6 @@ WARNING Missing required scopes: nc:write
|
|||||||
- Resource-level filtering (apply to MCP resources, not just tools)
|
- Resource-level filtering (apply to MCP resources, not just tools)
|
||||||
- Automatic scope discovery from decorated tools
|
- Automatic scope discovery from decorated tools
|
||||||
- Admin UI for scope management
|
- Admin UI for scope management
|
||||||
- Token introspection endpoint (RFC 7662)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user