Files
nextcloud-mcp-server/docs/jwt-oauth-reference.md
T
2025-10-23 12:22:34 +02:00

32 KiB

JWT OAuth Reference - Nextcloud MCP Server

Last Updated: 2025-10-23 Status: Production Ready

Table of Contents


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 - nc:read and nc: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 8959 endpoint for scope discovery
  • Backward Compatible - BasicAuth mode bypasses all scope checks

Supported Scopes

Scope Description Tool Count
nc:read Read-only access to Nextcloud data 36 tools
nc: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:

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:

{
  "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 nc:read nc: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
nc:read Read-only access Get notes, search files, list calendars, read contacts
nc: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

Full Access:

openid profile email nc:read nc:write

Read-Only:

openid profile email nc:read

No Custom Scopes (OIDC only):

openid profile email

Implementation

All 90 MCP tools are decorated with scope requirements:

@mcp.tool()
@require_scopes("nc:read")
async def nc_notes_get_note(note_id: int, ctx: Context):
    """Get a note by ID (requires nc:read scope)"""
    ...

@mcp.tool()
@require_scopes("nc:write")
async def nc_notes_create_note(title: str, content: str, ctx: Context):
    """Create a note (requires nc:write scope)"""
    ...

Coverage:

  • 36 read tools decorated with @require_scopes("nc:read")
  • 54 write tools decorated with @require_scopes("nc: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:

JWT with nc:read only:

  • list_tools() returns 36 read-only tools
  • Write tools are hidden from the tool list

JWT with nc:write only:

  • list_tools() returns 54 write-only tools
  • Read tools are hidden from the tool list

JWT with both scopes:

  • list_tools() returns all 90 tools

JWT with no custom scopes:

  • list_tools() returns 0 tools (all require nc:read or nc:write)

BasicAuth mode:

  • list_tools() returns all 90 tools (no filtering)

Scope Challenges

When a tool is called without required scopes, the server returns a 403 Forbidden response with a WWW-Authenticate header:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                  scope="nc:write",
                  resource_metadata="http://server/.well-known/oauth-protected-resource"

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 8959's Protected Resource Metadata endpoint:

Endpoint: GET /.well-known/oauth-protected-resource

Response:

{
  "resource": "http://localhost:8002",
  "scopes_supported": ["nc:read", "nc: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 three MCP server variants:

Service Port Auth Type Token Type Use Case
mcp 8000 BasicAuth N/A Development, testing
mcp-oauth 8001 OAuth Opaque Standard OAuth flows
mcp-oauth-jwt 8002 OAuth JWT Production, JWT testing

JWT Service Configuration

The mcp-oauth-jwt service uses Dynamic Client Registration (DCR) by default:

Default Configuration (DCR):

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_SCOPES=openid profile email nc:read nc:write
    - NEXTCLOUD_OIDC_TOKEN_TYPE=jwt
  volumes:
    - ./oauth-storage:/app/.oauth  # Optional: persist DCR credentials

With Pre-Configured Credentials:

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:

  • No credentials needed - DCR automatically registers the client on first start
  • Credentials persist - Saved to .nextcloud_oauth_client.json and reused
  • JWT tokens - Set TOKEN_TYPE=jwt for better performance
  • 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_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_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:

# 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:

{
  "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:

docker compose exec app php occ oidc:create \
  --token_type=jwt \
  --allowed_scopes="openid profile email nc:read nc:write" \
  "Nextcloud MCP Server" \
  "http://localhost:8000/oauth/callback"

Output:

{
  "client_id": "XBd2xqIisu3Kswg39Ub4BUhC36PEYjwwivx3G5nZdDgigvwKXrTHozs7m9DeoLSY",
  "client_secret": "xNKcy0qpUSau36T60pGGdb03pMEVLXtqykxjK8YkDpoNxNcZ4ClyAT3IAEse2AKT",
  "token_type": "jwt",
  "allowed_scopes": "openid profile email nc:read nc:write"
}

Configure MCP Server with Pre-Configured Credentials:

# 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: 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

Component Overview

┌──────────────────┐     OAuth Flow      ┌──────────────────┐
│  OAuth Client    │<─────────────────────>│ Nextcloud OIDC  │
│  (Claude, etc)   │                       │ Server           │
└────────┬─────────┘                       └────────┬─────────┘
         │                                          │
         │ JWT Access Token                         │
         │ {                                        │
         │   "scope": "openid nc:read nc: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:433-488)

  • Overrides FastMCP's list_tools() method
  • Filters based on user's JWT token scopes
  • 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
  • Advertises ["nc:read", "nc:write"]
  • RFC 8959 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 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):

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

Four test scenarios verify scope-based tool filtering with different consent levels:

1. No Custom Scopes (0 tools)

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 nc:read or nc:write) Verifies: Security - users who decline custom scopes cannot access any MCP tools

2. Read-Only Access (36 tools)

uv run pytest tests/server/test_scope_authorization.py::test_jwt_consent_scenarios_read_only -v

Scenario: JWT token with nc: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)

uv run pytest tests/server/test_scope_authorization.py::test_jwt_consent_scenarios_write_only -v

Scenario: JWT token with nc:write only Expected: 54 write tools visible, read tools hidden Verifies: Write tools accessible, read tools filtered out

4. Full Access (90 tools)

uv run pytest tests/server/test_scope_authorization.py::test_jwt_consent_scenarios_full_access -v

Scenario: JWT token with both nc:read and nc: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 nc:read only
  • write_only_oauth_client_credentials - Client with nc: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 nc:read
  • playwright_oauth_token_write_only - Obtains token with nc: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:

uv run pytest tests/server/test_scope_authorization.py -v

JWT OAuth integration tests:

uv run pytest tests/server/test_mcp_oauth_jwt.py -v --browser firefox

With visible browser (debugging):

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:

# 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:

# 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 nc:read nc:write" \
  "Client Name" \
  "http://callback/url"

Issue: All Tools Visible Despite Read-Only Token

Symptom: User with nc:read token can see all 90 tools including write tools

Cause: Server running in BasicAuth mode, not OAuth mode

Solution:

# Verify OAuth mode is active
docker compose logs mcp-oauth-jwt | grep "OAuth mode"

# Should see: "Running in OAuth mode"

# If not, check environment variables:
docker compose exec mcp-oauth-jwt 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:

# 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 nc:read nc: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. Delete .nextcloud_oauth_client.json 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:

# 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:

# Check server logs for OAuth mode
docker compose logs mcp-oauth-jwt | grep "WWW-Authenticate scope challenges enabled"

# Should see this during startup

# Check exception handling
docker compose logs mcp-oauth-jwt | grep "InsufficientScopeError"

Debugging Tools

Check JWT contents:

# Decode JWT (base64 decode the payload)
echo "JWT_PAYLOAD_PART" | base64 -d | jq .

Check database scopes:

# 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:

# Follow JWT verification logs
docker compose logs -f mcp-oauth-jwt | grep -E "JWT|scope|tool"

# Check for issuer mismatches
docker compose logs mcp-oauth-jwt | 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

# docker-compose.yml (production)
mcp-oauth-jwt:
  image: ghcr.io/yourusername/nextcloud-mcp-server:latest
  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 nc:read nc:write
    - NEXTCLOUD_OIDC_TOKEN_TYPE=jwt
  ports:
    - "8002:8002"

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:

# Success
INFO JWT verified successfully for user: admin
INFO ✅ Extracted scopes from access token: {'openid', 'profile', 'email', 'nc:read', 'nc:write'}

# Failures
WARNING JWT issuer validation failed: Invalid issuer
WARNING Missing required scopes: nc:write

Known Limitations

  1. No Fine-Grained Scopes - Only coarse nc:read and nc: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

External Resources


Implementation Date: 2025-10-21 to 2025-10-23 Version: 1.0.0 Status: Production Ready