Files
nextcloud-mcp-server/docs/oauth-impersonation-findings.md
Chris Coutinho e26c5128b7 docs: Reject service account tokens as OAuth authentication pattern
Service account tokens (client_credentials grant) violate OAuth "act on-behalf-of"
principles and have been moved to ADR-002's "Will Not Implement" section.

## Problem Discovery

Testing revealed that service account tokens create Nextcloud user accounts
(e.g., `service-account-nextcloud-mcp-server`) due to user_oidc's bearer
provisioning feature. This violates core OAuth principles:

-  Creates stateful server identity in Nextcloud
-  All actions attributed to service account, not real user
-  Breaks audit trail and user attribution
-  Service account becomes "admin by another name"

## Changes

### Documentation (ADR-002)
- Moved service account (old Tier 1) to "Will Not Implement" section
- Added "OAuth Act On-Behalf-Of Principle" section
- Renumbered tiers:
  - Tier 1: Impersonation (NOT IMPLEMENTED)
  - Tier 2: Delegation via token exchange (IMPLEMENTED)
- Updated status to reflect rejection of service accounts

### Code Warnings
- Added comprehensive warning to KeycloakOAuthClient.get_service_account_token()
- Clarified VALID use: only as subject_token for RFC 8693 token exchange
- Clarified INVALID use: direct API access with service account token

### Supporting Documentation
- CLAUDE.md: Removed outdated "Tier 1" references, added rejection note
- oauth-impersonation-findings.md: Added prominent update banner
- audience-validation-setup.md: Updated tier numbers, added rejection note
- tests/manual/test_token_exchange.py: Added warning comment

## Valid Patterns (ADR-002)

 Foreground operations: User's access token from MCP request
 Background operations: Token exchange (impersonation/delegation)
 Offline access: Refresh tokens with user consent
 Service accounts: Creates independent server identity (REJECTED)

## Alternative

If service account pattern is truly needed, use BasicAuth mode instead of
OAuth mode. OAuth mode MUST maintain "act on-behalf-of" semantics.

Related: c12df98 (revert of service account test)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:22 +01:00

12 KiB

OAuth Impersonation Investigation Findings

Date: 2025-11-02 Last Updated: 2025-11-02 (Token Exchange Resolution) Status: Implementation Complete - Token Exchange Working Conclusion: Keycloak Standard Token Exchange (RFC 8693) working for internal-to-internal token exchange. User impersonation requires Legacy V1.


⚠️ IMPORTANT UPDATE (2025-11-02)

This document contains outdated information regarding service account tokens.

After implementation and testing, we discovered that service account tokens (client_credentials grant) violate OAuth "act on-behalf-of" principles by creating Nextcloud user accounts (e.g., service-account-nextcloud-mcp-server). This approach has been REJECTED and moved to ADR-002's "Will Not Implement" section.

Key Changes:

  • Service account tokens (client_credentials) are INVALID - Creates user accounts, breaks audit trail
  • Token exchange (RFC 8693) is the correct approach - Implemented and working (ADR-002 Tier 2)
  • Offline access with refresh tokens - Still valid for background operations (ADR-002 primary approach)

For current architecture, see: docs/ADR-002-vector-sync-authentication.md


Summary

We investigated options for implementing user impersonation to enable background operations without requiring admin credentials (ADR-002 Tier 2). Here are the findings:

1. Keycloak Token Exchange (RFC 8693)

What We Implemented

  • Service account token acquisition (client_credentials grant)
  • get_service_account_token() method in KeycloakOAuthClient
  • exchange_token_for_user() method implementing RFC 8693
  • Token exchange configuration in Keycloak realm

What Works

Keycloak Standard V2 Token Exchange (RFC 8693) is WORKING:

  • Service account token acquisition via client_credentials grant
  • Token exchange for internal-to-internal tokens
  • Audience and scope modifications
  • Integration with Nextcloud APIs using exchanged tokens

Configuration Requirements: To enable Standard Token Exchange in Keycloak 26.2+, add to client attributes in realm-export.json:

"attributes": {
  "token.exchange.grant.enabled": "true",
  "client.token.exchange.standard.enabled": "true"
}

Limitations

Keycloak Standard V2 does NOT support:

  • User impersonation (requested_subject parameter)
  • Cross-client delegation (limited to same realm)

These features require Legacy V1 with --features=preview

Alternative: Keycloak Legacy V1

Keycloak Legacy Token Exchange (V1) WOULD support user impersonation, but:

  • Requires --features=preview --features=token-exchange flag
  • Not suitable for production
  • Deprecated and being phased out

Decision: Not viable for production use.


2. Nextcloud OIDC App Token Exchange

Discovery Endpoint Analysis

{
  "grant_types_supported": [
    "authorization_code",
    "implicit"
  ]
}

Findings

Nextcloud OIDC app does NOT support:

  • RFC 8693 token exchange
  • client_credentials grant
  • refresh_token grant (refresh tokens not issued)
  • User impersonation APIs

The Nextcloud OIDC app is a basic OAuth 2.0 provider focused on:

  • Authorization code flow for user login
  • JWT tokens for API access
  • Scope-based authorization

It is NOT designed for:

  • Service accounts
  • Token delegation
  • Background operations

Decision: Not viable - missing required grant types.


3. Nextcloud Impersonate App

What It Provides

Admin users can impersonate other users via:

  • UI: Settings → Users → Impersonate button
  • API: POST /apps/impersonate/user with userId parameter

How It Works

// From SettingsController.php
public function impersonate(string $userId): JSONResponse {
    // 1. Verify admin/delegated admin permissions
    // 2. Check target user has logged in before
    // 3. Set session: $this->userSession->setUser($impersonatee)
    // 4. Return success
}

Requirements

  • Admin credentials
  • Session-based authentication (cookies)
  • CSRF token
  • Target user must have logged in at least once
  • Not compatible with encryption-enabled instances

Limitations for Background Workers

Session-based, not stateless:

  • Requires maintaining HTTP session/cookies
  • Not suitable for distributed workers
  • Can't use with bearer tokens
  • Requires re-authentication periodically

Security concerns:

  • Requires admin credentials stored on server
  • All impersonated actions logged as target user
  • Violates principle of least privilege

Decision: Not suitable for background operations - session-based architecture incompatible with stateless OAuth/bearer token model.


4. What Actually Works

Option A: Admin Credentials (Current Implementation)

BasicAuth mode with admin account:

client = NextcloudClient.from_env()  # Uses NEXTCLOUD_USERNAME/PASSWORD
# Can access all APIs with admin permissions

Pros:

  • Simple, works immediately
  • Full access to all APIs

Cons:

  • Requires admin credentials stored on server
  • No per-user permission scoping
  • Security risk if credentials leaked
  • Violates ADR-002 goals

Status: Available but not recommended for production.

Option B: Service Account with Scoped Permissions

Create dedicated service account:

  1. Create mcp-sync user in Nextcloud
  2. Grant specific permissions (group memberships, shares)
  3. Use those credentials for background operations

Pros:

  • Dedicated account, easier to audit
  • Can limit permissions via Nextcloud groups
  • Works with current BasicAuth implementation

Cons:

  • Still requires credentials storage
  • Can't truly act "as" individual users
  • Limited by Nextcloud's permission model

Status: Best available option without OAuth delegation.


5. Recommendations

Short Term (Immediate)

Use Service Account Pattern:

# Background worker configuration
SYNC_ACCOUNT_USERNAME=mcp-sync
SYNC_ACCOUNT_PASSWORD=<secure-password>

# Create service account with limited permissions
docker compose exec app php occ user:add mcp-sync
docker compose exec app php occ group:adduser <appropriate-group> mcp-sync

Benefits:

  • Works with existing implementation
  • Better than admin credentials
  • Auditable

Medium Term (If OAuth Delegation Required)

Wait for proper standards support:

  • Monitor Keycloak for Standard V2 improvements
  • Contribute to/request Nextcloud OIDC app enhancements
  • Consider alternative identity providers (e.g., Authelia, Authentik)

Long Term (Ideal Solution)

Implement proper OAuth delegation:

  1. Use identity provider that supports RFC 8693 properly (e.g., Auth0, Okta)
  2. Or implement custom delegation endpoint in Nextcloud
  3. Or propose MCP protocol extension for refresh token sharing

6. Updated ADR-002 Status

Tier Solution Status Viability
Tier 0 Admin BasicAuth Implemented ⚠️ Works but not recommended
Tier 1 Offline Access (Refresh Tokens) ⚠️ Infrastructure ready MCP protocol limitation
Tier 2 Token Exchange (RFC 8693) WORKING Internal token exchange functional
Tier 3 Service Account (NEW) Available RECOMMENDED for background ops

7. Implementation Status

What Was Built

  1. RefreshTokenStorage - SQLite + encryption (ready for future use)
  2. KeycloakOAuthClient.get_service_account_token() - Works
  3. KeycloakOAuthClient.exchange_token_for_user() - Implemented but non-functional
  4. Token exchange configuration - Keycloak realm updated
  5. Test scripts - Comprehensive testing completed

What to Use

For Background Operations:

# Use service account with BasicAuth
from nextcloud_mcp_server.client import NextcloudClient

# In background worker
sync_client = NextcloudClient(
    base_url=os.getenv("NEXTCLOUD_HOST"),
    username=os.getenv("SYNC_ACCOUNT_USERNAME"),
    password=os.getenv("SYNC_ACCOUNT_PASSWORD"),
)

# Perform operations
notes = await sync_client.notes.search_notes("important")
# Index to vector database, etc.

For User Requests:

# Continue using OAuth bearer tokens
# Per-request client creation as currently implemented
client = get_client_from_context(ctx, nextcloud_host)

8. Files Modified/Created

Implementation

  • nextcloud_mcp_server/auth/keycloak_oauth.py - Token exchange methods
  • nextcloud_mcp_server/auth/refresh_token_storage.py - Token storage (ready for future)
  • nextcloud_mcp_server/app.py - OAuth configuration updates
  • keycloak/realm-export.json - Token exchange enabled
  • pyproject.toml - Added aiosqlite dependency

Documentation

  • docs/oauth-impersonation-findings.md - This document
  • docs/ADR-002-vector-sync-authentication.md - Original architecture decision

Tests

  • tests/manual/test_token_exchange.py - Keycloak RFC 8693 testing
  • tests/manual/test_nextcloud_impersonate.py - Nextcloud impersonate API testing

9. Conclusion

Neither Keycloak nor Nextcloud currently provide viable OAuth-based user impersonation for background operations.

The infrastructure is ready (token storage, exchange methods), but provider limitations prevent use.

Recommended approach: Use dedicated service account with appropriate Nextcloud permissions for background operations until proper OAuth delegation becomes available.

The implemented code remains valuable:

  • Ready for future when providers add support
  • Demonstrates proper OAuth patterns
  • Test infrastructure for validation

Appendix: Technical Details

Keycloak Configuration Applied

{
  "clientId": "nextcloud-mcp-server",
  "serviceAccountsEnabled": true,
  "attributes": {
    "token.exchange.grant.enabled": "true"
  }
}

Test Results - UPDATED (2025-11-02)

✅ Service account token acquisition: WORKS
✅ Token exchange discovery: SUPPORTED
✅ Token exchange configuration: ENABLED
✅ Actual token exchange: WORKS (after adding client.token.exchange.standard.enabled)
✅ Nextcloud API access: WORKS with exchanged tokens

Resolution: The realm-export.json was missing the client.token.exchange.standard.enabled attribute. After adding this attribute to keycloak/realm-export.json:128, token exchange works correctly on fresh Keycloak imports.

Nextcloud Impersonate Results

✓ App installation: SUCCESS
✓ Admin can impersonate: YES (session-based)
✗ Bearer token impersonate: NO (requires session cookies)
✗ Stateless impersonate: NOT AVAILABLE

10. Token Exchange Resolution (2025-11-02)

Problem

Initial token exchange implementation was failing with:

"Standard token exchange is not enabled for the requested client"

Root Cause

The realm-export.json was missing a critical attribute for Keycloak 26.2+ Standard Token Exchange:

  • Had: "token.exchange.grant.enabled": "true"
  • Missing: "client.token.exchange.standard.enabled": "true"

Fix Applied

Updated keycloak/realm-export.json at line 128 to include both attributes:

"attributes": {
  "pkce.code.challenge.method": "S256",
  "use.refresh.tokens": "true",
  "backchannel.logout.session.required": "true",
  "backchannel.logout.url": "http://app:80/index.php/apps/user_oidc/backchannel-logout/keycloak",
  "oauth2.device.authorization.grant.enabled": "false",
  "oidc.ciba.grant.enabled": "false",
  "client_credentials.use_refresh_token": "false",
  "display.on.consent.screen": "false",
  "token.exchange.grant.enabled": "true",
  "client.token.exchange.standard.enabled": "true"  // ADDED
}

Verification

After recreating Keycloak with fresh realm import:

$ docker compose down -v keycloak && docker compose up -d keycloak
$ uv run python tests/manual/test_token_exchange.py
✅ Token Exchange Test PASSED

Current Status

  • RFC 8693 Token Exchange fully functional
  • Service account token acquisition works
  • Token exchange for internal tokens works
  • Exchanged tokens validate with Nextcloud APIs
  • Realm import automatically applies correct configuration
  • ⚠️ User impersonation still requires Keycloak Legacy V1

Files Modified

  • keycloak/realm-export.json - Added client.token.exchange.standard.enabled attribute
  • docs/oauth-impersonation-findings.md - Updated with resolution

Testing

Run the complete token exchange flow:

uv run python tests/manual/test_token_exchange.py