Files
mcp-nextcloud/docs/oauth-impersonation-findings.md
Chris CoutinhoandClaude Opus 4.7 35c115ead6 docs: address remaining Login Flow v2 review feedback
Round-2 cleanup of PR #743 review comments not covered by d153e96:

- configuration.md: fix broken `#multi-user-oauth-modes` anchor; replace
  with the two real anchors (Multi-User BasicAuth, Login Flow v2). Rewrite
  the stale "always use OAuth2/OIDC with pre-configured clients" Best
  Practices section to reflect the post-pivot mode matrix, and update the
  Docker volume example to mount the encrypted app-password store
  (`TOKEN_STORAGE_DB`) rather than obsolete `.oauth` client storage.
- semantic-search-architecture.md: rename remaining body references from
  the deprecated `VECTOR_SYNC_ENABLED` to `ENABLE_SEMANTIC_SEARCH` so the
  doc matches configuration.md / troubleshooting.md.
- running.md: relabel "OAuth Mode (Recommended)" as
  "Login Flow v2 / OAuth issuer mode (--oauth)", drop the misleading
  "(Legacy)" suffix from BasicAuth, drop the
  `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` example (tied to the retired
  direct-OAuth-to-Nextcloud flow), and add a note explaining what
  `--oauth` actually enables post-pivot.
- keycloak-multi-client-validation.md, oauth-impersonation-findings.md:
  add a deprecation banner pointing at ADR-022 / Login Flow v2. Files
  retained because ADR-002 and CLAUDE.md still cite them.
- auth-flows.md: clarify under the Astrolabe → MCP diagram that the
  Nextcloud-OIDC JWKS path applies to Multi-User BasicAuth; under
  Login Flow v2 the MCP server validates tokens against its own JWKS.
- login-flow-v2.md: clarify the sticky-session note — affinity must key
  on the OAuth bearer token (or user-bound cookie), not source IP, since
  MCP clients may not maintain stable IPs across the provisioning flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:57:16 +02:00

13 KiB

OAuth Impersonation Investigation Findings

Warning

Deprecated — historical reference only. This document describes the direct-OAuth-to-Nextcloud / token-exchange architecture that was retired in favor of Login Flow v2. See ADR-022 and Login Flow v2 for the current approach. Retained because ADR-002 still cites this investigation for context.

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