Fix external IdP token exchange by using the correct audience identifier
for Keycloak.
Keycloak uses client IDs as audience identifiers, not URLs. The token
exchange was failing with "Audience not found" because it was requesting
audience "http://localhost:8080" but Keycloak only knows about the
"nextcloud" client ID.
Changes:
- Update mcp-keycloak service NEXTCLOUD_RESOURCE_URI from
"http://localhost:8080" to "nextcloud"
- Matches Keycloak's client ID convention for resource identifiers
- Token exchange now requests audience "nextcloud" which matches the
Keycloak resource server client configuration
Note: mcp-oauth service keeps URL-based resource URI because Nextcloud's
integrated OIDC app expects URLs, not client IDs. Different IdPs have
different conventions for audience/resource identifiers.
Test result: test_external_idp_token_validation now passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Wire up RFC 8693 token exchange throughout the MCP server to support
stateless per-request token conversion for external IdP scenarios.
Changes:
Authentication Flow:
- Add exchange_token_for_audience() for pure RFC 8693 exchange
- Update context_helper to use stateless token exchange
- Remove fallback to standard OAuth on exchange failure
- Make storage initialization lazy (only for delegation, not MCP tools)
Application Configuration:
- Add ENABLE_TOKEN_EXCHANGE environment variable support
- Skip provisioning tools when token exchange enabled
- Pass mcp_client_id to token broker for proper validation
- Update docker-compose.yml with token exchange config
Token Exchange Service:
- Add TOKEN_EXCHANGE_GRANT constant
- Implement exchange_token_for_audience() method
- Support both "mcp-server" and client_id audiences
- Lazy storage initialization for delegation scenarios
- Enhanced error handling and logging
Progressive Token Verifier:
- Add mcp_client_id parameter for external IdP validation
- Accept both "mcp-server" and configured client_id
- Support external IdP token verification
Key Behavior Changes:
- When ENABLE_TOKEN_EXCHANGE=true: Each MCP tool call triggers
stateless token exchange (client token → Nextcloud token)
- When ENABLE_TOKEN_EXCHANGE=false: Uses pass-through mode
(validates Flow 1 token and passes to Nextcloud)
- No provisioning tools registered in exchange mode
- No refresh tokens needed for request-time operations
This completes the token exchange implementation. The MCP server now
supports both pass-through (default) and exchange (opt-in) modes for
federated authentication architectures.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The test_adr004_hybrid_flow test expects Hybrid Flow mode where the MCP
server intercepts OAuth callbacks and stores refresh tokens. However,
ENABLE_PROGRESSIVE_CONSENT defaults to true, which causes the IdP to
redirect directly to the client, bypassing the MCP server callback.
This resulted in timeouts waiting for MCP authorization codes that never
arrived because the OAuth flow completed without server interception.
Sets ENABLE_PROGRESSIVE_CONSENT=false for mcp-oauth service to enable
Hybrid Flow mode for ADR-004 testing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
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>