268 Commits

Author SHA1 Message Date
Chris Coutinho 1a57f97d3a refactor: update to Qdrant query_points API and fix Playwright Keycloak login
- Replace deprecated qdrant_client.search() with query_points() API
- Update semantic search implementation in notes.py
- Update all integration tests to use query_points()
- Fix Keycloak login in test_keycloak_dcr.py to use form.submit() instead of button click
- Remove unnecessary popup handler code
- Simplify consent screen logging
2025-11-08 22:41:14 +01:00
Chris Coutinho 7b8c3f93a8 test: add integration tests for semantic search with in-process embeddings
Adds comprehensive integration tests for vector database semantic search that
work without external dependencies (Ollama), making them suitable for CI/CD.

Changes:
- Add SimpleEmbeddingProvider: in-process TF-IDF-like embeddings using feature hashing
- Make Ollama optional: embedding service now falls back to SimpleEmbeddingProvider
- Add 6 integration tests covering semantic search, filtering, and batch operations
- Downgrade urllib3 to 1.26.x for qdrant-client compatibility
- Update docker-compose.yml to comment out Ollama configuration (optional)

The SimpleEmbeddingProvider generates deterministic, normalized embeddings
suitable for testing semantic similarity without requiring external services.
Tests validate that similar texts have higher cosine similarity and that
semantic search correctly ranks results by relevance.

Test coverage:
- Deterministic embedding generation
- Semantic similarity between texts
- Full search flow with Qdrant (in-memory)
- Category filtering
- Empty result handling
- Batch embedding generation

All tests pass and can run in GitHub CI without Ollama infrastructure.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 22:13:33 +01:00
Chris Coutinho 4dbb2eb468 fix: integrate vector sync tasks with Starlette lifespan for streamable-http
Fixes background task startup for streamable-http transport by integrating
vector sync initialization into the Starlette lifespan context manager.

Starlette Lifespan Integration:
- Moved background task startup from FastMCP lifespan to Starlette lifespan
- FastMCP lifespan only triggers on MCP session establishment
- Starlette lifespan runs on server startup (correct timing)
- Fixed module scoping issues with local imports (anyio_module, asyncio_module)
- Added conditional startup based on oauth_enabled flag

Scanner Fixes:
- Fixed NotesClient method: list_notes() → get_all_notes()
- Properly handle AsyncIterator with list comprehension
- Collects all notes before processing

Verified Working:
- Background tasks start successfully on server startup
- Scanner fetches notes from Nextcloud API
- Processor pool (3 workers) ready for document processing
- Health endpoint reports Qdrant status
- No startup errors

Phase 3 Complete:
- BasicAuth mode with vector sync fully functional
- Background tasks integrate cleanly with streamable-http transport
- Graceful shutdown with coordinated task cancellation

Related: ADR-007 Background Vector Database Synchronization

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 21:20:26 +01:00
Chris Coutinho a766f4be32 test: enhance elicitation callback logging and error handling
Improve debugging capabilities for OAuth flow in elicitation callback:
- Add detailed logging for consent screen handling
- Capture screenshots when consent screen not detected or fails
- Replace networkidle wait with explicit callback URL polling
- Add 2-second grace period for server-side callback processing
- Log page title and current URL for debugging

This helps diagnose issues like expired OAuth clients or authorization failures during real elicitation testing.

Test results: All 4 elicitation integration tests passing
- test_check_logged_in_with_real_elicitation_callback ✓
- test_elicitation_callback_url_extraction ✓
- test_elicitation_stores_refresh_token ✓
- test_second_check_logged_in_does_not_elicit ✓

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 23:49:58 +01:00
Chris Coutinho 71326384da feat: add real elicitation integration test with python-sdk MCP client
This commit adds proper integration testing of the login elicitation flow
(ADR-006) using python-sdk's MCP client with actual elicitation callback
support, and fixes user_id extraction to support both JWT and opaque tokens.

## Changes

### 1. Enhanced create_mcp_client_session helper (tests/conftest.py)
- Added `elicitation_callback` parameter to function signature
- Pass callback to ClientSession constructor
- Added necessary imports: RequestContext, ElicitRequestParams,
  ElicitResult, ErrorData from mcp package
- Allows fixtures to provide custom elicitation handlers

### 2. New fixture: nc_mcp_oauth_client_with_elicitation (tests/conftest.py)
- Creates MCP client with Playwright-based elicitation callback
- Callback implementation:
  - Extracts OAuth URL from elicitation message using regex
  - Uses Playwright browser to complete OAuth flow automatically
  - Handles Nextcloud login form (username/password)
  - Handles consent screen if present
  - Waits for OAuth callback completion
  - Returns ElicitResult(action="accept") on success
- Function-scoped to allow independent test state
- Tracks elicitation invocations via session.elicitation_triggered

### 3. Fixed user_id extraction for opaque tokens (oauth_tools.py)
- Created extract_user_id_from_token() helper to handle both JWT and
  opaque tokens by calling userinfo endpoint when needed
- Fixed check_logged_in to use helper instead of broken ctx.authorization
- Fixed revoke_nextcloud_access to use helper instead of ctx.context.get()
- Both tools now properly extract user_id from access tokens

### 4. Enhanced integration tests (test_elicitation_integration.py)
- Updated tests to revoke refresh tokens via MCP tool
- All 4 tests now pass:
  - test_check_logged_in_with_real_elicitation_callback: Complete flow
  - test_elicitation_callback_url_extraction: URL extraction validation
  - test_elicitation_stores_refresh_token: Token persistence verification
  - test_second_check_logged_in_does_not_elicit: No redundant elicitations

### 5. Added diagnostic logging (oauth_routes.py)
- Track user_id extraction from ID tokens during OAuth callbacks
- Log refresh token storage with user_id and flow_type

## Test Results
 4/4 tests pass

The test suite successfully validates:
- Elicitation callback is triggered when no refresh token exists
- Playwright automation completes OAuth flow
- Refresh token is stored after OAuth with correct user_id
- Tool returns "yes" after successful login
- Already-logged-in users don't get redundant elicitations

## Why This Matters
Previous tests (test_login_elicitation.py) only validated response
formats and acknowledged they couldn't test real elicitation protocol.

This test exercises the REAL MCP elicitation protocol end-to-end:
1. MCP server calls ctx.elicit()
2. python-sdk ClientSession invokes custom callback
3. Callback completes OAuth via Playwright
4. Client returns acceptance to server
5. Tool proceeds with authenticated state

This proves the python-sdk MCP client can handle elicitation in
production environments with both JWT and opaque tokens.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 22:55:49 +01:00
Chris Coutinho 281d28c7cd test: Add comprehensive elicitation URL and refresh token validation
Enhanced test suite to validate:
1. Elicitation URL format and Flow 2 endpoint routing
2. Server-side refresh token validation via check_provisioning_status API
3. Proper separation of concerns - tests use MCP server API, not direct storage access

The refresh token validation test validates server responses:
- is_provisioned=true: Server has valid refresh token
- is_provisioned=false: No token or invalid token
- Error response: Token validation failed

This ensures the MCP server properly validates refresh tokens internally
and reports status correctly through its public API.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 21:21:58 +01:00
Chris Coutinho 0c9a9ea24d fix: Consolidate OAuth callbacks and implement PKCE for all flows
This PR fixes multiple OAuth-related issues:

## Unified OAuth Callback
- Consolidated `/oauth/callback-nextcloud` and `/oauth/login-callback` into single `/oauth/callback` endpoint
- Flow type determined by session lookup via state parameter (no query params in redirect_uri)
- Fixes redirect_uri validation issues with IdPs requiring exact match
- Legacy endpoints kept as aliases for backwards compatibility

## PKCE Implementation
- Implemented PKCE (RFC 7636) for Flow 2 (resource provisioning)
  - Generate code_verifier and code_challenge
  - Store code_verifier in session storage
  - Retrieve and use in token exchange
- Fixed PKCE for browser login (integrated mode)
  - Previously only worked for external IdP (Keycloak)
  - Now works for both Nextcloud OIDC and external IdP

## Login Elicitation Fixes (ADR-006)
- Fixed elicitation URL to route through MCP server endpoint
  - Changed from direct Nextcloud URL to `/oauth/authorize-nextcloud`
  - Ensures PKCE is properly handled by server
- Fixed login detection after OAuth flow completes
  - Look up refresh token by state parameter instead of user_id
  - Works even when Flow 1 token not present
- Added `get_refresh_token_by_provisioning_client_id()` method

## Session Authentication
- Fixed `/user/page` redirect loop
  - Shared oauth_context with mounted browser_app
  - SessionAuthBackend can now validate sessions correctly

## Tests
- Added comprehensive login elicitation test suite
- Updated scope authorization test expectations
- All 43 OAuth tests passing

## Files Changed
- `app.py`: Shared oauth_context, unified callback route
- `oauth_routes.py`: Unified callback, PKCE for Flow 2
- `browser_oauth_routes.py`: PKCE for integrated mode
- `oauth_tools.py`: Fixed elicitation URL generation
- `refresh_token_storage.py`: Added lookup by provisioning_client_id
- `test_login_elicitation.py`: New test suite

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 21:08:55 +01:00
Chris Coutinho 659087e4c7 fix: Implement proper OAuth resource parameters and PRM-based discovery
This commit completes the OAuth audience validation implementation per RFC 7519,
RFC 8707 (Resource Indicators), and RFC 9728 (Protected Resource Metadata).

## Key Changes

### OAuth Resource Parameters (RFC 8707)
- Add `resource` parameter to Flow 1 (MCP client auth) with MCP server audience
- Add `resource` parameter to Flow 2 (Nextcloud access) with Nextcloud audience
- Add `nextcloud_resource_uri` to oauth_context configuration
- Fix undefined variable error in starlette_lifespan

### PRM-Based Resource Discovery (RFC 9728)
- Update tests to fetch resource identifier from PRM endpoint
- Add fallback to hardcoded value if PRM fetch fails
- Demonstrate correct OAuth client implementation pattern

### ADR-005 Documentation Updates
- Update to reflect simplified RFC 7519 compliant implementation
- Document that MCP validates only its own audience (not Nextcloud's)
- Add section on OAuth resource parameters and PRM discovery
- Update implementation checklist to show completed items
- Mark status as "Implemented" with update date

## Implementation Details

The solution follows RFC 7519 Section 4.1.3: resource servers validate only
their own presence in the audience claim. This simplifies the logic while
maintaining security:

- MCP server validates MCP audience only
- Nextcloud independently validates its own audience
- No dual validation required at MCP layer
- Token reuse is allowed per RFC 8707 for multi-audience tokens

## Test Results
 test_mcp_oauth_server_connection - PASSED
 test_deck_board_view_permissions - PASSED
 test_prm_endpoint - PASSED

All OAuth flows now properly specify target resources, resulting in correct
audience validation throughout the system.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 23:19:03 +01:00
Chris Coutinho bdb1ba2051 refactor: Eliminate duplicate validation logic in UnifiedTokenVerifier
Since both multi-audience and exchange modes now validate the same thing
(MCP audience only per RFC 7519), consolidated the duplicate methods:

- Removed duplicate verification methods (_verify_multi_audience_token
  and _verify_mcp_audience_only)
- Created single _verify_mcp_audience() method for all validation
- Removed duplicate helper (_validate_multi_audience), kept _has_mcp_audience
- Mode only affects logging and what happens AFTER verification

The mode distinction is now purely about post-verification behavior:
- Multi-audience mode: Use token directly (Nextcloud validates its own)
- Exchange mode: Exchange for Nextcloud-audience token via RFC 8693

This makes the code cleaner and clearer about what's actually happening -
both modes do identical validation, they just differ in how the validated
token is used.

All tests pass: unit (65), OAuth integration confirmed working.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 21:58:52 +01:00
Chris Coutinho 7d9ab5559c fix: Simplify token verifier to be RFC 7519 compliant
Per RFC 7519 Section 4.1.3, resource servers should only validate their
own presence in the audience claim, not check for other resource servers.

Changes:
- UnifiedTokenVerifier now validates only MCP audience (not Nextcloud's)
- Nextcloud independently validates its own audience when receiving API calls
- This is NOT token passthrough (we validate tokens before use)
- This IS token reuse which is explicitly allowed by RFC 8707

Updates:
- Simplified _validate_multi_audience() to follow OAuth spec
- Updated docstrings and comments to clarify RFC 7519 compliance
- Fixed unit tests that expected dual-audience validation
- Updated ADR-005 to document the correct OAuth interpretation
- All tests pass: unit (65), smoke (5), OAuth integration

This makes the implementation simpler, more maintainable, and properly
aligned with OAuth 2.0 specifications while maintaining security.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 21:44:04 +01:00
Chris Coutinho 9fab6cb550 feat: Implement ADR-005 unified token verifier to eliminate token passthrough vulnerability
Replace two non-compliant token verifiers (NextcloudTokenVerifier and
ProgressiveConsentTokenVerifier) with a single UnifiedTokenVerifier that properly
validates token audiences per MCP Security Best Practices specification.

The previous implementation had a critical security vulnerability where tokens
intended for the MCP server were passed directly to Nextcloud APIs without
proper audience validation (token passthrough anti-pattern). This violates
OAuth 2.0 security principles and the MCP specification.

Changes:
- Add UnifiedTokenVerifier supporting two compliant modes:
  * Multi-audience mode (default): Validates tokens contain BOTH MCP and
    Nextcloud audiences, enabling direct use without exchange
  * Token exchange mode (opt-in): Validates MCP audience only, exchanges
    for Nextcloud tokens via RFC 8693 with caching to minimize latency

- Remove token passthrough vulnerability from context.py and context_helper.py
- Implement token exchange caching (5-minute TTL default) to reduce network calls
- Add required environment variables for audience validation:
  * NEXTCLOUD_MCP_SERVER_URL - MCP server URL (used as audience)
  * NEXTCLOUD_RESOURCE_URI - Nextcloud resource identifier
  * TOKEN_EXCHANGE_CACHE_TTL - Cache TTL for exchanged tokens

- Update docker-compose.yml with resource URI configuration for both OAuth modes
- Add comprehensive test suite (29 tests) covering both authentication modes
- Remove legacy NextcloudTokenVerifier and ProgressiveConsentTokenVerifier

Security improvements:
- Eliminates token passthrough anti-pattern
- Enforces proper audience separation between MCP and Nextcloud
- Complies with MCP Security Best Practices and RFC 8707/8693
- Maintains performance with token exchange caching

Test results: 65/65 unit tests passed, 5/5 smoke tests passed

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 18:53:14 +01:00
Chris Coutinho 1675fc521b fix: use valid Fernet encryption keys in token exchange tests
Fix three tests in test_token_exchange.py that were using invalid
Fernet encryption keys (b"test-key-" + b"0" * 32), causing ValueError
due to invalid base64 encoding.

Root cause:
- Tests manually created invalid Fernet keys
- token_storage and token_broker fixtures generated different keys
- Encryption/decryption operations failed due to key mismatch

Solution:
- Expose valid encryption key from token_storage fixture via _test_encryption_key
- Update token_broker fixture to use same encryption key from token_storage
- Update all tests to use token_storage._test_encryption_key

Tests fixed:
- test_get_background_token
- test_session_background_separation
- test_background_token_different_scopes

All 13 tests in test_token_exchange.py now pass.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:06:06 +01:00
Chris Coutinho dec02f17d1 test: remove Bearer token tests for browser-only /user* endpoints
Remove test_userinfo_integration.py which incorrectly expected Bearer token
authentication to work with /user and /user/page endpoints.

Root cause:
- /user* endpoints are designed for browser-based session authentication
- SessionAuthBackend only accepts session cookies, not Bearer tokens
- Tests were passing Authorization: Bearer headers which cannot work

The /user* endpoints are part of the browser admin UI and require:
1. Login via /oauth/login to establish session cookie
2. Session cookie in subsequent requests to /user or /user/page

Browser-based integration tests using Playwright (if needed) should test
the full OAuth login flow with session cookies, not direct Bearer token access.

Tests removed: 13 tests (all using Bearer tokens incorrectly)
Remaining OAuth tests: 77 tests still passing

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:47:19 +01:00
Chris Coutinho 881b0ba03c feat: add scope protection to OAuth provisioning tools
Add @require_scopes("openid") decorator to OAuth backend tools
(provision_nextcloud_access, revoke_nextcloud_access, check_provisioning_status)
to ensure they're only visible to authenticated OIDC users.

Design rationale:
- OAuth provisioning tools are "meta-tools" that manage authentication itself
- They don't access Nextcloud resources, so don't need resource scopes
- Requiring 'openid' ensures user is authenticated via OIDC
- Enables Progressive Consent: users authenticate first, then provision access
- Aligns with dual OAuth flow architecture (Flow 1 + Flow 2)

Changes:
- Add @require_scopes("openid") to all three OAuth provisioning tools
- Update test expectations: users with only OIDC default scopes
  see OAuth provisioning tools but not resource tools
- All tests pass (13/13 in test_scope_authorization.py)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:25:20 +01:00
Chris Coutinho 192c4bf009 fix: correct OAuth token audience validation using RFC 8707 resource parameter
The test_mcp_oauth_server_connection test was failing because OAuth tokens
had the wrong audience claim. The MCP server's progressive_token_verifier
expects tokens with audience matching its OAuth client ID, but tokens were
being issued with Nextcloud's default resource server audience.

Changes:

1. Test fixtures (tests/conftest.py):
   - Add get_mcp_server_resource_metadata() helper to fetch PRM metadata
   - Update playwright_oauth_token to include resource parameter in auth requests
   - Update _get_oauth_token_with_scopes to support optional resource parameter
   - Automatically fetch resource ID from MCP server's PRM endpoint

2. MCP Server (nextcloud_mcp_server/app.py):
   - Fix Protected Resource Metadata endpoint to return OAuth client ID
   - Change "resource" field from URL to client ID for proper audience validation
   - Ensures tokens obtained with resource parameter have correct audience claim

How it works:
1. Test fetches /.well-known/oauth-protected-resource from MCP server
2. Extracts resource field (MCP server's client ID)
3. Includes &resource=<client-id> in OAuth authorization request (RFC 8707)
4. Nextcloud OIDC issues tokens with aud: [<client-id>]
5. MCP server's progressive_token_verifier accepts tokens (audience matches)

Fixes OAuth test failures:
- test_mcp_oauth_server_connection
- test_mcp_oauth_tool_execution
- test_mcp_oauth_client_with_playwright

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 03:06:11 +01:00
Chris Coutinho 0ff85dbe4f feat: implement RFC 8693 Standard Token Exchange for Keycloak
Configure Keycloak 26.4.2 realm to support Standard Token Exchange V2,
enabling the MCP server to exchange client tokens (aud: nextcloud-mcp-server)
for Nextcloud-scoped tokens (aud: nextcloud) via RFC 8693.

Changes:
- Remove duplicate audience workarounds from realm configuration
- Add token-exchange-nextcloud client scope with audience mapper
- Configure scope as default for nextcloud-mcp-server client
- Enable standard.token.exchange.enabled on both clients
- Add comprehensive integration tests (7 tests, all passing)

Token Exchange Flow:
1. Client obtains token with aud: [nextcloud-mcp-server, nextcloud]
2. Server exchanges to aud: nextcloud, azp: nextcloud-mcp-server
3. Exchanged token used for Nextcloud API calls
4. Each request gets fresh ephemeral token (stateless)

Key Implementation Details:
- Uses Keycloak 26.2+ scope-based authorization (no FGAP required)
- Target audiences must be in client's default/optional scopes
- Protocol mappers alone don't grant exchange permission
- Tokens expire after 300s (5 minutes)

Tests validate:
- Basic token exchange flow
- Nextcloud API integration (Capabilities, Notes)
- CRUD operations with exchanged tokens
- Multiple stateless exchanges from same client token
- Token claims preservation (aud, azp, sub)
- Scope configuration validation

See docs/ADR-004-progressive-consent.md for architecture details.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 02:30:37 +01:00
Chris Coutinho 15113dbb03 fix: remove Hybrid Flow, make Progressive Consent default (ADR-004)
Eliminates scope escalation security vulnerability by removing Hybrid Flow
and making Progressive Consent the only OAuth mode.

Changes:
- Delete oauth_callback() and oauth_token() (Hybrid Flow only, ~314 lines)
- Fix scope flows: Flow 1 requests resource scopes, Flow 2 requests identity+offline
- Remove ENABLE_PROGRESSIVE_CONSENT flag (always enabled in OAuth mode)
- Update documentation to reflect Progressive Consent as default
- Delete test_adr004_hybrid_flow.py test file
- Remove unused variables (ruff lint fixes)

Security improvements:
- No scope escalation: client gets exactly what it requests
- Clear separation: MCP session tokens vs Nextcloud offline tokens
- OAuth2 compliant: follows best practices for scope handling

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 00:26:07 +01:00
Chris Coutinho d14f2f666d feat: Add userinfo route/page 2025-11-04 00:03:24 +01:00
Chris Coutinho d92945a388 test: fix async context manager mocking in userinfo tests
Fixes test_query_idp_userinfo tests to properly mock httpx.AsyncClient
context manager by adding __aenter__ and __aexit__ to the mock.

Also skips remaining tests that rely on old API signature - these are
now covered by integration tests in test_userinfo_integration.py.

Test results:
- 2 passing unit tests for _query_idp_userinfo
- 12 skipped tests for old API (covered by integration tests)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 22:50:31 +01:00
Chris Coutinho 42426b4597 fix: browser OAuth userinfo endpoint and refresh token rotation
Fixes two critical issues in browser OAuth flow for admin UI:

1. Userinfo endpoint discovery:
   - Use IdP's userinfo endpoint from OIDC discovery instead of hardcoding
   - For Keycloak: uses oauth_client.userinfo_endpoint
   - For Nextcloud: queries discovery document at runtime
   - Fixes 404 errors when querying user profile

2. Refresh token rotation:
   - Update stored refresh tokens after successful refresh
   - Fixes "Could not find access token for code or refresh_token" errors
   - Enables persistent sessions across page refreshes
   - Applies to both Keycloak and Nextcloud integrated modes

Test updates:
   - Skip outdated unit tests that relied on old API signature
   - Browser OAuth flow is covered by integration tests

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 22:46:19 +01:00
Chris Coutinho c2dcb06fe1 feat: add browser-based user info page with separate OAuth flow
Implements /user and /user/page endpoints for displaying authenticated
user information in both BasicAuth and OAuth modes.

Key Features:
- Separate browser OAuth flow (/oauth/login, /oauth/login-callback, /oauth/logout)
- Session-based authentication using signed cookies
- Token refresh for persistent sessions
- HTML and JSON user info endpoints
- IdP profile information retrieval

Architecture:
- BasicAuth mode: Always authenticated as configured user
- OAuth mode: Browser-based authorization code flow with refresh tokens
- Session stored in SQLite with encrypted refresh tokens
- Server-side token refresh using internal Docker hostnames

OAuth Flow:
- /oauth/login: Initiates browser OAuth flow
- /oauth/login-callback: Handles IdP callback and stores refresh token
- /oauth/logout: Clears session cookie
- /user: JSON API endpoint (requires authentication)
- /user/page: HTML page endpoint (requires authentication)

DCR Scopes Fix:
- MCP server DCR now only requests basic OIDC scopes (openid profile email offline_access)
- Nextcloud app scopes (notes:read, etc.) are for MCP clients, not the server itself
- PRM endpoint dynamically advertises supported scopes from tool decorators

Files:
- nextcloud_mcp_server/auth/browser_oauth_routes.py: Browser OAuth flow handlers
- nextcloud_mcp_server/auth/session_backend.py: Starlette session authentication
- nextcloud_mcp_server/auth/userinfo_routes.py: User info endpoints with token refresh
- tests/server/auth/test_userinfo_routes.py: Unit tests
- tests/server/oauth/test_userinfo_integration.py: OAuth integration tests

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 22:16:49 +01:00
Chris Coutinho 71e77e95bc refactor: integrate token exchange into unified get_client() pattern
Resolves the token exchange implementation gap where get_session_client()
was implemented but never used by tools. Unifies token acquisition into a
single async get_client() method that handles both pass-through and token
exchange modes transparently.

Core Changes:
- Make get_client() async and merge token exchange logic into it
- Remove scopes parameter from token exchange (Nextcloud doesn't support OAuth scopes)
- Update all 8 tool modules to use await get_client(ctx)
- Fix provisioning decorator to skip checks in BasicAuth mode

Token Acquisition Modes:
1. BasicAuth: Returns shared client (no token operations)
2. OAuth pass-through (default): Verifies and passes Flow 1 token to Nextcloud
3. OAuth token exchange (opt-in): Exchanges Flow 1 token for ephemeral token via RFC 8693

Key Architectural Clarifications:
- Progressive Consent (Flow 1/2) = Authorization architecture
- Token Exchange = Token acquisition pattern during tool execution
- Refresh tokens from Flow 2 are NEVER used for tool calls (only background jobs)
- Nextcloud scopes are "soft-scopes" enforced by MCP server, not IdP

Documentation Updates:
- ADR-004: Added comprehensive token acquisition patterns section
- CRITICAL-TOKEN-EXCHANGE-PATTERN.md: Updated to reflect implementation status
- CLAUDE.md: Updated architectural patterns with async get_client()

Testing:
- All 36 unit tests passing
- All 4 smoke tests passing (BasicAuth mode)
- Linting issues fixed (ruff)

Configuration:
ENABLE_TOKEN_EXCHANGE=false (default) - pass-through mode
ENABLE_TOKEN_EXCHANGE=true (opt-in) - token exchange mode

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 20:33:56 +01:00
Chris Coutinho d16bcdcfbb feat: Implement ADR-004 Progressive Consent foundation components
- Token Broker Service manages Nextcloud access tokens with audience validation
- Implements short-lived token caching (5-minute TTL) with early refresh
- Enhanced token storage schema with ADR-004 fields (flow_type, audience, provisioning)
- MCP provisioning tools for explicit Flow 2 resource authorization
- Comprehensive unit tests for Token Broker Service (14 tests, all passing)
- Environment configuration for Progressive Consent mode

This implements the foundation for the dual OAuth flow architecture where:
- Flow 1: MCP clients authenticate to MCP server (aud: "mcp-server")
- Flow 2: MCP server gets delegated Nextcloud access (aud: "nextcloud")

Users must explicitly call provision_nextcloud_access tool to grant resource access,
implementing the "stateless by default" principle from ADR-004.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 07:51:07 +01:00
Chris Coutinho babd60e08b feat: Implement ADR-004 Hybrid Flow with comprehensive integration tests
Implement the ADR-004 Hybrid Flow OAuth pattern where the MCP server
intercepts the OAuth callback to obtain master refresh tokens while
maintaining PKCE security for clients.

## Implementation

### OAuth Routes (ADR-004 Hybrid Flow)
- Add `/oauth/authorize` endpoint: Intercepts client OAuth initiation
- Add `/oauth/callback` endpoint: Receives IdP callback, stores master token
- Add `/oauth/token` endpoint: Exchanges MCP code for client access token
- Implement PKCE code challenge/verifier validation
- Store OAuth sessions with state/challenge correlation

### MCP Server Integration
- Update `setup_oauth_config()` to return client_id and client_secret
- Initialize OAuth context in Starlette lifespan for login routes
- Add OAuth session storage to RefreshTokenStorage
- Configure authlib dependency for OAuth flow management

### Integration Tests
- Create `test_adr004_hybrid_flow.py` with Playwright automation
- Add `adr004_hybrid_flow_mcp_client` session-scoped fixture
- Test MCP session establishment with hybrid flow token
- Test tool execution using stored refresh tokens (on-behalf-of pattern)
- Test persistent access across multiple operations
- All tests passing:  3 passed in 8.82s

### Documentation
- Update ADR-004 with comprehensive Testing section
- Add integration test commands and coverage details
- Document test implementation and verification steps
- Create TESTING_INSTRUCTIONS.md for manual and automated testing
- Include manual test scripts for reference/debugging

## What This Enables

 PKCE code challenge/verifier flow
 MCP server intercepts OAuth callback and stores master refresh token
 Client receives MCP access token (not master token)
 MCP session establishment with hybrid flow token
 Tool execution using stored refresh tokens (on-behalf-of pattern)
 Multiple operations without re-authentication
 Proper token isolation (client never sees master token)

## Testing

Run ADR-004 integration tests:
```bash
uv run pytest tests/server/oauth/test_adr004_hybrid_flow.py --browser firefox -v
```

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 02:18:30 +01:00
Chris Coutinho 34df5f5b9a feat: Implement dual-tier token exchange (Standard V2 + Legacy V1 impersonation)
This commit implements and documents both RFC 8693 token exchange tiers
from ADR-002, enabling both production-ready delegation and advanced
impersonation capabilities.

- Enable Keycloak preview features (`--features=preview`) to support
  both Standard V2 and Legacy V1 token exchange modes

- Update Tier 1 status from "NOT IMPLEMENTED" to "IMPLEMENTED (Legacy V1)"
- Add detailed empirical testing results showing:
  - Standard V2 rejects `requested_subject` parameter
  - Legacy V1 accepts parameter but requires impersonation permissions
  - Complete configuration steps for enabling impersonation
- Add comparison table showing when to use each tier
- Add "When to Use" guidance for both tiers
- Document that Tier 2 (Delegation) is the recommended default

- Update docstring to document both Tier 1 and Tier 2 support
- Add tier-specific logging (shows which tier is being used)
- Document permission requirements for Tier 1 impersonation

**tests/integration/auth/test_token_exchange_standard_v2.py**:
- Test delegation without impersonation (Tier 2)
- Verify sub claim remains unchanged (service account identity)
- Verify no special permissions required
- Test exchanged tokens work with Nextcloud APIs
- All tests PASS 

**tests/integration/auth/test_token_exchange_legacy_v1.py**:
- Test impersonation with `requested_subject` (Tier 1)
- Verify sub claim changes to target user
- Auto-skip if impersonation permissions not configured
- Document permission requirements in test docstrings
- Test exchanged tokens work with Nextcloud APIs

**tests/manual/test_impersonation.py**:
- Comprehensive impersonation validation script
- Tests both Standard V2 and Legacy V1 behavior
- Decodes JWT tokens to verify sub claim changes
- Validates tokens against Nextcloud APIs

**tests/manual/configure_impersonation.py**:
- Automated permission configuration helper
- Documents manual Keycloak CLI configuration steps

Both token exchange tiers are now fully implemented and tested:

- **Tier 2 (Delegation)** -  RECOMMENDED
  - Standard V2 (production-ready)
  - No special permissions required
  - Service account identity preserved

- **Tier 1 (Impersonation)** -  Advanced use only
  - Legacy V1 (--features=preview required)
  - Requires manual permission grant via Keycloak CLI
  - Subject claim changes to target user

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:22 +01:00
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
Chris Coutinho ed813af45c Revert "test: Add automated test for service account token acquisition (ADR-002 Tier 1)"
This reverts commit cbc37f1d76687d66a771236903ccb88b2e7b0242.
2025-11-02 22:03:22 +01:00
Chris Coutinho 1e071c83a9 test: Add automated test for service account token acquisition (ADR-002 Tier 1)
Add comprehensive automated integration test for Keycloak service account
token acquisition via client_credentials grant, validating ADR-002 Tier 1
implementation for external IdP mode.

Changes:
- Add keycloak_oauth_client fixture in tests/conftest.py
  - Creates KeycloakOAuthClient instance for service account operations
  - Session-scoped fixture with automatic cleanup
  - Discovers Keycloak endpoints automatically

- Add test_keycloak_service_account_token_acquisition test
  - Tests client_credentials grant token acquisition
  - Verifies token response structure (access_token, token_type, expires_in)
  - Validates token works with Nextcloud APIs via capabilities endpoint
  - Documents limitation for Nextcloud OIDC app (integrated mode)

- Update ADR-002 documentation
  - Mark automated test as complete ()
  - Document supported providers (Keycloak , Nextcloud OIDC app )
  - Add note that KeycloakOAuthClient is provider-agnostic
  - Clarify that Nextcloud OIDC app support requires config only

Test results:
-  Service account token acquired successfully (300s expiry, Bearer type)
-  Token validated by Nextcloud user_oidc app
-  Token works with Nextcloud capabilities API

Note: Nextcloud OIDC app (integrated mode) service account token support
not yet implemented. See app.py:631-635 for current status.

Resolves: "TODO: Automated integration tests needed for both Keycloak and
Nextcloud OIDC app" from ADR-002
2025-11-02 22:03:22 +01:00
Chris Coutinho 23360485a8 refactor: Remove NEXTCLOUD_OIDC_CLIENT_STORAGE environment variable
Remove the NEXTCLOUD_OIDC_CLIENT_STORAGE environment variable from all
configuration files. OAuth client credentials are now always stored in the
SQLite database, with no option to use a custom JSON file path.

Changes:
- Remove NEXTCLOUD_OIDC_CLIENT_STORAGE from .env.keycloak.sample
- Remove NEXTCLOUD_OIDC_CLIENT_STORAGE from docker-compose.yml (mcp-oauth and mcp-keycloak services)
- Remove NEXTCLOUD_OIDC_CLIENT_STORAGE from Helm deployment template
- Remove NEXTCLOUD_OIDC_CLIENT_STORAGE from test_cli.py test assertions
- Remove --headed flag from pytest addopts (use CLI arg instead)

This simplifies configuration by enforcing a single storage mechanism
(SQLite database) for OAuth client credentials.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:21 +01:00
Chris Coutinho 4c7d1cfc8d test: Add scope-based authorization tests for Keycloak external IdP
This enhances the Keycloak integration test suite with comprehensive
scope-based authorization validation, matching the OIDC test structure.

Changes:
- Add 3 test users to Keycloak realm (read-only, write-only, no-custom-scopes)
- Create OAuth token fixtures with different scope combinations
- Create MCP client fixtures for each scope configuration
- Add 4 new tests validating scope-based tool filtering:
  * Read-only tokens filter out write tools
  * Write-only tokens filter out read tools
  * Full access tokens show all 90+ tools
  * No custom scopes result in zero tools

Test Results:
- All 15 Keycloak integration tests pass (11 existing + 4 new)
- Validates proper JWT scope enforcement in external IdP architecture
- Confirms security isolation when users decline custom scopes

This completes ADR-002 scope authorization testing for the Keycloak
external identity provider integration.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:21 +01:00
Chris Coutinho 849c67c32a fix: Complete Keycloak external IdP integration with all tests passing
This commit completes the Keycloak external IdP integration for the MCP
server, implementing ADR-002 Tier 2 (External Identity Provider) with
full Bearer token authentication support.

Key Changes:
1. **Keycloak backchannel-dynamic configuration**
   - Added --hostname-strict=false and --hostname-backchannel-dynamic=true
   - Allows external issuer (localhost:8888) with internal endpoints (keycloak:8080)
   - Solves Docker networking issue where containers can't reach localhost

2. **CORSMiddleware Bearer token patch**
   - Created app-hooks/patches/cors-bearer-token.patch from upstream commit 8fb5e77db82
   - Allows Bearer tokens to bypass CORS/CSRF checks (stateless authentication)
   - Applied via post-installation hook 20-apply-cors-bearer-token-patch.sh
   - Enables app-specific APIs (Notes, Calendar, etc.) to work with Bearer tokens

3. **Patch organization**
   - Moved patches to app-hooks/patches/ directory
   - Updated docker-compose.yml to mount entire app-hooks directory
   - Consolidated patch management for better maintainability

4. **Test improvements**
   - All 11 Keycloak integration tests passing
   - Tests validate OAuth token acquisition, MCP connectivity, token validation,
     tool execution, token persistence, user provisioning, scope filtering,
     and error handling

Architecture:
- Keycloak acts as external OAuth/OIDC identity provider
- MCP server uses Keycloak tokens to access Nextcloud APIs
- Nextcloud user_oidc app validates Bearer tokens from Keycloak
- No admin credentials needed - all API access uses user's OAuth tokens

Cache Note:
- Discovery and JWKS caches must be cleared when switching Keycloak configurations
- Use: docker compose exec redis redis-cli DEL "<cache-key>"
- Or: docker compose exec app php occ user_oidc:provider keycloak --clientid nextcloud

Related:
- ADR-002: Vector sync background jobs authentication
- Validates external IdP integration pattern
- Demonstrates offline_access with refresh tokens (Tier 1 & 2)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:20 +01:00
Chris Coutinho 6117aaaed3 fix: Complete Keycloak external IdP integration with all tests passing
This commit completes the Keycloak external identity provider integration,
implementing the ADR-002 architecture where Keycloak acts as an external
OAuth/OIDC provider and Nextcloud validates tokens via the user_oidc app.

Architecture:
  MCP Client → Keycloak (OAuth) → MCP Server → Nextcloud user_oidc → APIs

Key Fixes:

1. Keycloak JWT token configuration
   - Added 'sub' claim protocol mapper to realm-export.json
   - Updated token_verifier.py to accept both 'sub' and 'preferred_username'
   - Ensures tokens contain required OIDC claims

2. Keycloak hostname configuration for Docker networking
   - Implemented --hostname-backchannel-dynamic=true in docker-compose.yml
   - External clients use localhost:8888 (public)
   - Internal services use keycloak:8080 (Docker network)
   - Same issuer (localhost:8888) everywhere for token consistency
   - Restored frontendUrl in realm attributes

3. MCP server provider mode detection
   - Fixed URL normalization to handle port differences (http://app vs http://app:80)
   - Correctly distinguishes integrated mode vs external IdP mode
   - Removes explicit default ports (80 for HTTP, 443 for HTTPS)

4. Nextcloud SSRF protection configuration
   - Added allow_local_remote_servers=true to user_oidc install script
   - Enables Nextcloud to fetch JWKS from internal Keycloak container
   - Required for external IdP token validation

5. OAuth lifespan cleanup
   - Fixed RefreshTokenStorage close() error (uses context managers)
   - Added safe cleanup for oauth_client with hasattr check
   - Prevents session crash on shutdown

6. Test suite fixes
   - Fixed test_user_auto_provisioning to reflect actual behavior
   - Fixed test_scope_filtering_with_keycloak tool name (nc_webdav_write_file)
   - Updated test_keycloak_oauth_client_credentials_discovery for hostname config
   - All 11 Keycloak external IdP tests now passing

Testing:
   All 11 tests in test_keycloak_external_idp.py passing
   OAuth token acquisition via Playwright automation
   Token validation through Nextcloud user_oidc app
   Write operations (Notes create, Calendar create, File upload)
   Read operations (search, list, get)
   Token persistence across multiple operations
   User authentication and bearer token validation
   Scope-based tool filtering
   Error handling for invalid operations

Implementation validates:
  - ADR-002 external identity provider architecture
  - No admin credentials needed in MCP server
  - Centralized identity management via Keycloak
  - Standards-based OAuth 2.0 / OIDC integration
  - User auto-provisioning from IdP claims

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:20 +01:00
Chris Coutinho 403f8be429 feat: Add Keycloak external IdP integration with custom scopes
Add comprehensive support for using Keycloak as an external identity
provider with Nextcloud custom scopes. This enables testing of ADR-002
external IdP integration patterns.

**Keycloak Realm Configuration:**
- Add frontendUrl attribute to issue tokens with public issuer URL
- Define 18 Nextcloud custom client scopes (notes:read/write,
  calendar:read/write, contacts:read/write, cookbook:read/write,
  deck:read/write, tables:read/write, files:read/write,
  sharing:read/write, todo:read/write)
- Add all custom scopes to nextcloud-mcp-server client optional scopes
- Scopes include consent screen text for user-friendly OAuth flow

**MCP Server Configuration:**
- Add OIDC_JWKS_URI environment variable support
- Implement JWKS URI override logic for Docker networking
- Update NEXTCLOUD_PUBLIC_ISSUER_URL to include full realm path
- Enable MCP server to fetch JWKS from internal Docker network

**Test Infrastructure:**
- Add keycloak_oauth_client_credentials fixture (session-scoped)
- Add keycloak_oauth_token fixture with Playwright automation
- Implement PKCE (S256) support for Keycloak OAuth flow
- Add nc_mcp_keycloak_client fixture for MCP testing
- Create comprehensive test suite in test_keycloak_external_idp.py

**Tests Created:**
- test_keycloak_oauth_token_acquisition: Token acquisition via Playwright
- test_keycloak_oauth_client_credentials_discovery: OIDC discovery
- test_mcp_client_connects_to_keycloak_server: MCP connectivity
- test_external_idp_server_initialization: Server auto-detection
- test_external_idp_token_validation: Token validation flow
- test_tools_work_with_keycloak_token: End-to-end tool execution
- test_keycloak_token_persistence: Multi-operation token reuse
- test_user_auto_provisioning: Nextcloud user provisioning
- test_scope_filtering_with_keycloak: Scope-based tool filtering
- test_keycloak_error_handling: Error handling
- test_external_idp_architecture: Architecture documentation

**Current Status:**
-  Keycloak realm configuration complete
-  Custom scopes defined and available
-  OAuth token acquisition working (1 test passing)
- ⚠️  Token validation needs additional work (external IdP userinfo)

**Files Modified:**
- keycloak/realm-export.json: Realm configuration with scopes
- tests/conftest.py: Keycloak OAuth fixtures (+285 lines)
- tests/server/oauth/test_keycloak_external_idp.py: New test suite
- docker-compose.yml: OIDC_JWKS_URI and issuer configuration
- nextcloud_mcp_server/app.py: JWKS URI override logic

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:20 +01:00
Chris Coutinho e331544cee feat: Implement RFC 8693 token exchange for Keycloak (ADR-002 Tier 2)
Implements OAuth 2.0 Token Exchange (RFC 8693) enabling the MCP server to
exchange service account tokens for user-scoped tokens. This provides an
alternative to refresh tokens for background operations.

**Core Implementation:**
- Added `get_service_account_token()` method to KeycloakOAuthClient for
  client_credentials grant
- Added `exchange_token_for_user()` method implementing RFC 8693 token exchange
- Fixed Fernet encryption key handling in RefreshTokenStorage (was incorrectly
  base64 decoding already-encoded keys)
- Updated OAuth configuration to support offline_access scope and refresh token
  storage infrastructure

**Keycloak Configuration:**
- Enabled `serviceAccountsEnabled` in realm-export.json
- Added `token.exchange.grant.enabled` attribute
- Added `client.token.exchange.standard.enabled` attribute (required for
  Keycloak 26.2+ Standard Token Exchange V2)
- Fresh Keycloak imports now correctly enable token exchange

**Docker Compose:**
- Added TOKEN_ENCRYPTION_KEY and ENABLE_OFFLINE_ACCESS environment variables
- Created oauth-tokens volume for refresh token storage
- Configured both mcp-oauth and mcp-keycloak services

**Testing & Documentation:**
- Added tests/manual/test_token_exchange.py - Validates complete RFC 8693 flow
- Added tests/manual/test_nextcloud_impersonate.py - Documents session-based
  impersonation limitations
- Added docs/oauth-impersonation-findings.md - Comprehensive investigation
  findings and resolution documentation

**Verified Working:**
 Service account token acquisition (client_credentials grant)
 RFC 8693 token exchange for internal-to-internal tokens
 Exchanged tokens validate with Nextcloud APIs
 Keycloak 26.4.2 Standard Token Exchange V2 support

**Known Limitations:**
- User impersonation (requested_subject) requires Keycloak Legacy V1 with
  preview features
- Cross-client token exchange limited to same realm
- Refresh token storage infrastructure ready but unused (MCP protocol limitation)

Dependencies: aiosqlite>=0.20.0

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:19 +01:00
Chris Coutinho 37b0b4a281 fix: Update DCR token_type tests for OIDC app changes
The Nextcloud OIDC app has updated token_type parameter values:
- Changed from "Bearer" → "opaque" for opaque tokens
- Changed from "JWT" → "jwt" for JWT tokens

Updated test_dcr_token_type.py to use lowercase token_type values:
- token_type="jwt" for JWT-formatted tokens
- token_type="opaque" for opaque/bearer tokens

This fixes test failures where tests were using the old "Bearer" and
"JWT" (uppercase) values which are no longer recognized by the OIDC app.

Fixes test: test_dcr_respects_bearer_token_type

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 22:03:19 +01:00
Chris Coutinho ab7411d9fd test: Fix tests 2025-10-25 22:07:46 +02:00
Chris Coutinho 9414d9c9c3 test: Add integration marker to user/group tests 2025-10-25 20:16:14 +02:00
Chris Coutinho 8a52df4a8e test: Skip unstructured tests if not enabled 2025-10-25 20:13:41 +02:00
Chris Coutinho a36038422b feat: Add text processing background worker for telling client about progress 2025-10-25 19:52:45 +02:00
Chris Coutinho 2147fc1696 refactor: Transform document parsing into pluggable processor architecture
Refactors PR #190's hardcoded Unstructured.io integration into a flexible,
extensible plugin system supporting multiple text extraction engines.

- **`DocumentProcessor` ABC**: Abstract interface for all processors
- **`ProcessorRegistry`**: Central registry for discovery and routing
- **`ProcessingResult`**: Standardized output format across processors

- **`UnstructuredProcessor`**: Refactored from `UnstructuredClient`
- **`TesseractProcessor`**: Local OCR for images (lightweight alternative)
- **`CustomHTTPProcessor`**: Generic wrapper for custom HTTP APIs

- New `get_document_processor_config()` returns structured config
- Supports enabling/disabling individual processors
- Per-processor configuration via environment variables
- **Breaking Change**: `ENABLE_UNSTRUCTURED_PARSING` replaced with:
  - `ENABLE_DOCUMENT_PROCESSING=true/false` (master switch)
  - `ENABLE_UNSTRUCTURED=true/false` (per-processor)
  - `ENABLE_TESSERACT=true/false`
  - `ENABLE_CUSTOM_PROCESSOR=true/false`

- `parse_document()` now uses `ProcessorRegistry`
- Auto-selects appropriate processor based on MIME type
- Processor priority system (Unstructured=10, Tesseract=5, Custom=1)

- `initialize_document_processors()` registers processors at startup
- Integrated into both BasicAuth and OAuth lifespans
- Graceful degradation if processors fail to initialize

```env
ENABLE_DOCUMENT_PROCESSING=false

ENABLE_UNSTRUCTURED=false
UNSTRUCTURED_API_URL=http://unstructured:8000
UNSTRUCTURED_STRATEGY=auto  # auto|fast|hi_res
UNSTRUCTURED_LANGUAGES=eng,deu

ENABLE_TESSERACT=false
TESSERACT_LANG=eng

ENABLE_CUSTOM_PROCESSOR=false
CUSTOM_PROCESSOR_URL=http://localhost:9000/process
CUSTOM_PROCESSOR_TYPES=application/pdf,image/jpeg
```

- **Removed**: `tests/test_unstructured_config.py` (legacy tests)
- **Added**: `tests/unit/test_document_processor_config.py`
  - 7 unit tests for new config system
  - Tests individual and multi-processor configurations

- **Added**:
  - `nextcloud_mcp_server/document_processors/__init__.py`
  - `nextcloud_mcp_server/document_processors/base.py`
  - `nextcloud_mcp_server/document_processors/registry.py`
  - `nextcloud_mcp_server/document_processors/unstructured.py`
  - `nextcloud_mcp_server/document_processors/tesseract.py`
  - `nextcloud_mcp_server/document_processors/custom_http.py`
  - `tests/unit/test_document_processor_config.py`

- **Modified**:
  - `nextcloud_mcp_server/config.py` - New plugin config system
  - `nextcloud_mcp_server/app.py` - Processor initialization
  - `nextcloud_mcp_server/utils/document_parser.py` - Uses registry
  - `nextcloud_mcp_server/server/webdav.py` - Import updates
  - `env.sample` - New configuration format
  - `docker-compose.yml` - (profile changes from previous work)

- **Removed**:
  - `nextcloud_mcp_server/client/unstructured_client.py` - Replaced by UnstructuredProcessor
  - `tests/test_unstructured_config.py` - Replaced with new tests

 **Extensible**: Add processors without modifying core code
 **Testable**: Mock processors for unit tests
 **Configurable**: Enable only needed processors
 **Flexible**: Choose fast (Tesseract) vs accurate (Unstructured)
 **Opt-in**: Disabled by default, no mandatory dependencies

Users upgrading from PR #190 need to update environment variables:
```bash
ENABLE_UNSTRUCTURED_PARSING=true

ENABLE_DOCUMENT_PROCESSING=true
ENABLE_UNSTRUCTURED=true
```

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 19:28:35 +02:00
yuisheaven f0e5333e43 Merge branch 'master' into feature/introduce_files_parsing_with_unstructured_service_for_webdav_files_retrieval 2025-10-25 17:23:38 +02:00
Chris Coutinho 01b43c96ba test: Update client id/secret -> client_info 2025-10-24 19:47:49 +02:00
Chris Coutinho 50b69a2531 fix: Add support for RFC 7592 client registration and deletion 2025-10-24 19:19:27 +02:00
Chris Coutinho 8e0a4d8ce5 feat(auth): Add support for client registration deletion 2025-10-24 18:54:24 +02:00
Chris Coutinho 72fce189d2 test: Add tests for dcr endpoint and update oidc app 2025-10-24 18:48:05 +02:00
Chris Coutinho 1e877f17f7 test: Replace persistent OAuth client cache with session-scoped fixtures
Remove file-based caching of OAuth client credentials and implement automatic
client lifecycle management for test fixtures.

Changes:
- Add RFC 7592 client deletion function in auth/client_registration.py
- Remove cache_file parameter from _create_oauth_client_with_scopes helper
- Update all OAuth credential fixtures to use yield/finalizer pattern
- Add automatic client cleanup at end of test session (best-effort)
- Remove persistent .nextcloud_oauth_*.json cache files

Benefits:
- No persistent cache files cluttering repository
- Fresh OAuth clients created for each test session via DCR
- Automatic cleanup attempts (RFC 7592 DELETE endpoint)
- Cleaner test environment with proper fixture lifecycle

Note: Client deletion may fail due to Nextcloud authentication middleware
(logged as warning). The key improvement is removing persistent cache files.
OAuth clients may accumulate in Nextcloud but can be cleaned manually.
2025-10-24 08:11:22 +02:00
Chris Coutinho 13f76a7734 chore: Upgrade pydantic Config to ConfigDict 2025-10-24 06:18:13 +02:00
Chris Coutinho 2f1bd1bbe9 test: Move client integration tests to mocked unit tests 2025-10-24 05:50:25 +02:00
Chris Coutinho d452684535 feat: Split read/write scopes into app:read/write scopes 2025-10-24 04:38:49 +02:00
Chris Coutinho d55e5708c7 ci: fix imports 2025-10-24 01:04:30 +02:00