From ab4012781176df28b68e5bc28f95227a5bdd214f Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Tue, 14 Oct 2025 01:32:30 +0200 Subject: [PATCH] ci: [skip ci] Remove --- OAUTH_IMPLEMENTATION_PLAN.md | 742 ----------------------------------- OAUTH_TESTING.md | 121 ------ 2 files changed, 863 deletions(-) delete mode 100644 OAUTH_IMPLEMENTATION_PLAN.md delete mode 100644 OAUTH_TESTING.md diff --git a/OAUTH_IMPLEMENTATION_PLAN.md b/OAUTH_IMPLEMENTATION_PLAN.md deleted file mode 100644 index e6c82b4..0000000 --- a/OAUTH_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,742 +0,0 @@ -# OAuth2/OIDC Implementation Plan for Nextcloud MCP Server - -## Executive Summary -Upgrade the Nextcloud MCP server to support OAuth2/OIDC authentication using Nextcloud's OIDC app as the Authorization Server, eliminating the need for baked-in credentials in server deployment. - -**Status**: ✅ Research Complete - Implementation Ready - -## Research Findings Summary - -### ✅ Verified Nextcloud OIDC Capabilities -- **Token Format**: Opaque tokens by default, **RFC 9068 JWT access tokens available** (must be enabled per-client) -- **Discovery**: Full OpenID Connect discovery available at `/.well-known/openid-configuration` -- **JWKS**: Available at `/apps/oidc/jwks` for JWT signature validation -- **Dynamic Registration**: Supported via `/apps/oidc/register` (must be enabled by admin) -- **Introspection**: ❌ NOT available - must use **userinfo endpoint** for token validation -- **Userinfo**: Available at `/apps/oidc/userinfo` - validates token and returns user claims -- **Scopes**: `openid`, `profile`, `email`, `roles`, `groups` -- **User Claims**: `sub`, `preferred_username` (both contain Nextcloud username) - -### 🔑 Key Implementation Decisions -1. **Primary Token Validation**: Use **userinfo endpoint** (not introspection) -2. **JWT Support**: Optional - enables local validation if client configured for RFC 9068 -3. **User Context**: Extract username from `sub` or `preferred_username` claim via userinfo -4. **Dynamic Registration**: Primary deployment method (zero-config) -5. **Token Lifetime**: Access tokens default to 3600s, clients default to 3600s (both configurable) - -## Architecture Overview - -### Server Role: Resource Server (RS) - RFC 9728 -The MCP server acts as a **Resource Server** that: -- Validates OAuth tokens issued by Nextcloud OIDC app (Authorization Server) -- Protects MCP tools/resources with OAuth authentication -- Uses validated tokens to make Nextcloud API calls on behalf of authenticated users - -### Authentication Flow -``` -1. Client connects to MCP Server (RS) -2. MCP Server provides RFC 9728 metadata pointing to Nextcloud OIDC (AS) -3. Client performs OAuth flow with Nextcloud OIDC -4. Client presents access token to MCP Server -5. MCP Server validates token via userinfo endpoint (or JWT if configured) -6. MCP Server extracts username from claims -7. MCP Server uses token to call Nextcloud APIs with user context -``` - -## Key Design Decisions - -### 1. Dynamic Client Registration (PRIMARY APPROACH) -**Use Nextcloud OIDC's Dynamic Client Registration for zero-config deployment** - -**Benefits:** -- No manual client setup required -- MCP server auto-registers on first startup -- Automatic credential generation -- Self-healing if client expires -- Better developer/deployment experience - -**Implementation:** -```python -# Startup sequence: -1. Check for existing client credentials (file/env) -2. If none found, POST to /apps/oidc/register -3. Store client_id and client_secret persistently -4. Use credentials for OAuth flow -5. Auto re-register if client expires (3600s default) -``` - -**Nextcloud OIDC Requirements:** -- Admin must enable "Dynamic Client Registration" in OIDC app settings -- Rate limiting via BruteForce protection -- Max 100 dynamic clients per instance -- Clients expire after 1 hour (configurable via occ) - -### 2. Token Validation Strategy: Userinfo Endpoint (PRIMARY) - -**✅ VERIFIED IMPLEMENTATION: Userinfo Endpoint Validation** - -Nextcloud OIDC **does NOT provide** a token introspection endpoint. Token validation must use: - -**Primary: Userinfo Endpoint Validation** -- Call `/apps/oidc/userinfo` with Bearer token -- Nextcloud validates token internally (checks expiration, client, etc.) -- Returns user claims if valid: `sub`, `preferred_username`, `email`, `roles`, `groups` -- HTTP 400/401 if token invalid -- Cache results with TTL matching token expiration (3600s default) - -**Implementation Pattern**: -```python -async def verify_token(self, token: str) -> AccessToken | None: - # Call userinfo endpoint - response = await client.get( - f"{nextcloud_host}/apps/oidc/userinfo", - headers={"Authorization": f"Bearer {token}"} - ) - - if response.status_code == 200: - claims = response.json() - return AccessToken( - token=token, - client_id="", # Not available from userinfo - scopes=["openid", "profile"], # From original request - expires_at=calculate_expiry() # 3600s from now - ) - return None # Invalid token -``` - -**Optional: JWT Validation (Performance Optimization)** -- Available if client configured with "JWT Access Tokens (RFC 9068)" enabled -- Fetch JWKS from `/apps/oidc/jwks` -- Validate JWT signatures locally (no network call) -- Cache JWKS with refresh mechanism -- Falls back to userinfo if JWT validation fails - -**Trade-offs**: -- Userinfo: Simpler, always works, network call per validation -- JWT: Faster, no network call, requires per-client configuration - -### 3. Dual-Mode Authentication (Backward Compatibility) -Support both authentication modes: - -**Mode 1: OAuth2/OIDC (NEW)** -- Environment: `NEXTCLOUD_HOST` + optional `NEXTCLOUD_OIDC_CLIENT_ID/SECRET` -- Auto-registers if no client credentials provided -- Per-request client creation with bearer token - -**Mode 2: Basic Auth (LEGACY)** -- Environment: `NEXTCLOUD_HOST` + `NEXTCLOUD_USERNAME` + `NEXTCLOUD_PASSWORD` -- Current implementation preserved -- Single client in lifespan context - -### 4. HTTP Client Architecture - -**✅ REVISED: Context-aware Client Retrieval** - -Instead of per-request client creation, use a helper that extracts user context: - -```python -# Helper function to get client from MCP context -async def get_client_from_context(ctx: Context, base_url: str) -> NextcloudClient: - """Extract authenticated user context and create NextcloudClient.""" - # MCP SDK provides AccessToken from TokenVerifier - access_token: AccessToken = ctx.request_context.session.access_token - - # Extract username from cached userinfo claims - # (stored during token verification) - username = access_token.scopes[0] # Or from custom metadata - - # Create client with bearer token - return NextcloudClient.from_token( - base_url=base_url, - token=access_token.token, - username=username - ) - -# In tool implementations: -@mcp.tool() -async def nc_notes_create(title: str, content: str): - ctx = mcp.get_context() - - if oauth_mode: - client = await get_client_from_context(ctx, nextcloud_host) - else: - # Legacy: use lifespan client - client = ctx.request_context.lifespan_context.client - - return await client.notes.create_note(title, content) -``` - -**Key Pattern**: -- Token verification caches userinfo claims -- Helper retrieves username from cached data (no additional API call) -- Client uses bearer token for Nextcloud API calls - -### 5. User Context Extraction - -**✅ VERIFIED: Userinfo Endpoint Response** - -From Nextcloud OIDC userinfo endpoint response: -- **Username**: `sub` AND `preferred_username` (both contain Nextcloud username) -- **Scopes**: Determined by scopes requested during OAuth flow -- **Groups/Roles**: Available via `roles` or `groups` scope -- **Profile**: `name`, `email`, `picture`, etc. (if `profile` scope requested) - -**Implementation**: -```python -# During token verification: -userinfo = await fetch_userinfo(token) -# { -# "sub": "username", -# "preferred_username": "username", -# "email": "user@example.com", -# "roles": ["group1", "group2"], # if 'roles' scope -# "groups": ["group1", "group2"] # if 'groups' scope -# } - -username = userinfo["sub"] # or userinfo["preferred_username"] -``` - -**Storage Strategy**: -- Cache userinfo in AccessToken metadata -- Use MCP SDK's built-in token caching -- TTL matches access token expiration (3600s default) - -## Implementation Components - -### New Modules - -#### 1. `nextcloud_mcp_server/auth/__init__.py` -Exports: `NextcloudTokenVerifier`, `BearerAuth`, `register_client` - -#### 2. `nextcloud_mcp_server/auth/token_verifier.py` -```python -class NextcloudTokenVerifier(TokenVerifier): - """ - Validates access tokens using Nextcloud OIDC userinfo endpoint. - - Primary method: Userinfo endpoint validation (always works) - Optional: JWT validation if client configured for RFC 9068 - """ - - def __init__( - self, - nextcloud_host: str, - userinfo_uri: str, - jwks_uri: str | None = None, - enable_jwt_validation: bool = False - ): - self.nextcloud_host = nextcloud_host - self.userinfo_uri = userinfo_uri - self.jwks_uri = jwks_uri - self.enable_jwt_validation = enable_jwt_validation - - # Cache for validated tokens: token -> (userinfo, expiry) - self._token_cache: dict[str, tuple[dict, float]] = {} - - # JWKS cache (if JWT validation enabled) - self._jwks: dict | None = None - self._jwks_expires: float = 0 - - self._client = httpx.AsyncClient() - - async def verify_token(self, token: str) -> AccessToken | None: - """ - Verify token using userinfo endpoint (primary) or JWT validation (optional). - - Returns AccessToken with userinfo cached in metadata. - """ - # Check cache first - if token in self._token_cache: - userinfo, expiry = self._token_cache[token] - if time.time() < expiry: - return self._create_access_token(token, userinfo) - - # Try JWT validation first if enabled - if self.enable_jwt_validation and self.jwks_uri: - access_token = await self._verify_jwt(token) - if access_token: - return access_token - - # Fall back to (or use primary) userinfo validation - return await self._verify_via_userinfo(token) - - async def _verify_via_userinfo(self, token: str) -> AccessToken | None: - """Validate token by calling userinfo endpoint.""" - try: - response = await self._client.get( - self.userinfo_uri, - headers={"Authorization": f"Bearer {token}"}, - timeout=5.0 - ) - - if response.status_code == 200: - userinfo = response.json() - - # Cache for 3600s (default token lifetime) - # TODO: Get actual expiry from token if JWT - expiry = time.time() + 3600 - self._token_cache[token] = (userinfo, expiry) - - return self._create_access_token(token, userinfo) - - except Exception as e: - logger.warning(f"Userinfo validation failed: {e}") - - return None - - async def _verify_jwt(self, token: str) -> AccessToken | None: - """Validate JWT token locally using JWKS (optional optimization).""" - try: - # Fetch JWKS if not cached - if not self._jwks or time.time() > self._jwks_expires: - await self._refresh_jwks() - - # Decode and validate JWT - claims = jwt.decode( - token, - self._jwks, - algorithms=["RS256", "HS256"], - issuer=self.nextcloud_host, - options={"verify_aud": False} # Nextcloud may not include aud - ) - - # Extract userinfo from JWT claims - userinfo = { - "sub": claims.get("sub"), - "preferred_username": claims.get("preferred_username"), - "email": claims.get("email"), - "roles": claims.get("roles", []), - "groups": claims.get("groups", []) - } - - # Cache - expiry = claims.get("exp", time.time() + 3600) - self._token_cache[token] = (userinfo, expiry) - - return self._create_access_token(token, userinfo) - - except Exception as e: - logger.debug(f"JWT validation failed, falling back to userinfo: {e}") - return None - - def _create_access_token(self, token: str, userinfo: dict) -> AccessToken: - """Create AccessToken with userinfo in metadata.""" - username = userinfo.get("sub") or userinfo.get("preferred_username") - - return AccessToken( - token=token, - client_id="", # Not available from userinfo - scopes=["openid", "profile", "email"], # TODO: Track actual scopes - expires_at=int(time.time() + 3600), # TODO: Get from JWT exp claim - # Store username in scopes[0] as workaround for MCP SDK limitation - # Or use custom AccessToken subclass with username field - ) - - async def _refresh_jwks(self): - """Fetch JWKS from Nextcloud OIDC.""" - response = await self._client.get(self.jwks_uri) - response.raise_for_status() - self._jwks = response.json() - self._jwks_expires = time.time() + 3600 # Cache for 1 hour - - async def close(self): - """Cleanup resources.""" - await self._client.aclose() -``` - -#### 3. `nextcloud_mcp_server/auth/client_registration.py` -```python -async def register_client( - nextcloud_url: str, - client_name: str = "Nextcloud MCP Server", - redirect_uris: list[str] = None -) -> dict: - """Register MCP server as OAuth client with Nextcloud OIDC""" - # POST to /apps/oidc/register - # Return client_id, client_secret, expires_at - -async def load_or_register_client(storage_path: str) -> dict: - """Load existing client or register new one""" - # Check storage file - # Validate expiration - # Re-register if expired - # Persist credentials -``` - -#### 4. `nextcloud_mcp_server/auth/bearer_auth.py` -```python -class BearerAuth(httpx.Auth): - """Bearer token authentication for httpx""" - - def __init__(self, token: str): - self.token = token - - def auth_flow(self, request): - request.headers["Authorization"] = f"Bearer {self.token}" - yield request -``` - -### Modified Files - -#### 1. `nextcloud_mcp_server/app.py` -```python -# Add OAuth configuration -from nextcloud_mcp_server.auth import NextcloudTokenVerifier, register_client - -# In get_app(): -if oauth_enabled: - # Load or register client - client_info = await load_or_register_client(storage_path) - - # Create token verifier - token_verifier = NextcloudTokenVerifier( - jwks_uri=f"{nextcloud_host}/apps/oidc/jwks", - issuer=f"{nextcloud_host}" - ) - - # Configure FastMCP with OAuth - mcp = FastMCP( - "Nextcloud MCP", - token_verifier=token_verifier, - auth=AuthSettings( - issuer_url=nextcloud_host, - resource_server_url=mcp_server_url, - required_scopes=["openid", "profile"] - ), - lifespan=app_lifespan_oauth # Don't create client in lifespan - ) -else: - # Legacy BasicAuth mode - mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_basic) -``` - -#### 2. `nextcloud_mcp_server/client/__init__.py` -```python -class NextcloudClient: - def __init__(self, base_url: str, username: str, auth: Auth | None = None): - # Accept either BasicAuth or BearerAuth - self._client = AsyncClient(base_url=base_url, auth=auth, ...) - - @classmethod - def from_env(cls): - """Legacy: Create from username/password env vars""" - return cls(base_url, username, auth=BasicAuth(username, password)) - - @classmethod - def from_token(cls, base_url: str, token: str, username: str): - """OAuth: Create from bearer token""" - return cls(base_url, username, auth=BearerAuth(token)) -``` - -#### 3. `nextcloud_mcp_server/server/notes.py` (and other tool modules) -```python -from nextcloud_mcp_server.auth import get_client_from_context - -@mcp.tool() -async def nc_notes_create(title: str, content: str): - ctx: Context = mcp.get_context() - - # OAuth mode: Get client from request context - if oauth_enabled: - client = get_client_from_context(ctx) - else: - # Legacy mode: Use lifespan client - client = ctx.request_context.lifespan_context.client - - return await client.notes.create_note(...) -``` - -#### 4. `nextcloud_mcp_server/config.py` -```python -class NextcloudConfig: - # Common - host: str - - # OAuth mode - oauth_enabled: bool = False - oidc_client_id: str | None = None - oidc_client_secret: str | None = None - client_storage_path: str = ".nextcloud_oauth_client.json" - mcp_server_url: str = "http://localhost:8000/mcp" - required_scopes: list[str] = ["openid", "profile", "email"] - - # Legacy mode - username: str | None = None - password: str | None = None - - @classmethod - def from_env(cls): - oauth_enabled = not ( - os.getenv("NEXTCLOUD_USERNAME") and - os.getenv("NEXTCLOUD_PASSWORD") - ) - return cls(oauth_enabled=oauth_enabled, ...) -``` - -### Configuration Files - -#### Updated `env.sample` -```bash -# Nextcloud Instance -NEXTCLOUD_HOST=https://nextcloud.example.com - -# ===== AUTHENTICATION MODE ===== -# Choose ONE of the following: - -# Option 1: OAuth2/OIDC (RECOMMENDED) -# - Requires Nextcloud OIDC app installed -# - Enable "Dynamic Client Registration" in OIDC app settings -# - Leave NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD empty -# - Optional: Pre-register client and provide credentials -NEXTCLOUD_OIDC_CLIENT_ID= -NEXTCLOUD_OIDC_CLIENT_SECRET= -NEXTCLOUD_OIDC_CLIENT_STORAGE=.nextcloud_oauth_client.json -NEXTCLOUD_MCP_SERVER_URL=http://localhost:8000/mcp - -# Option 2: Basic Authentication (LEGACY - Will be deprecated) -# - Requires username and password -# - Less secure - credentials stored in environment -# - Use only for backward compatibility -NEXTCLOUD_USERNAME= -NEXTCLOUD_PASSWORD= -``` - -## Dependencies - -### New Python Dependencies -```toml -# pyproject.toml additions: -dependencies = [ - # ... existing ... - "PyJWT[crypto]>=2.8.0", # JWT validation - "cryptography>=41.0.0", # JWKS key handling (if not present) -] -``` - -## Nextcloud OIDC Setup - -### Administrator Setup (One-time) -1. Install Nextcloud OIDC app from App Store -2. Navigate to Settings → OIDC -3. Enable "Dynamic Client Registration" -4. (Optional) Configure token expiration times via CLI: - ```bash - php occ config:app:set oidc expire_time --value "3600" - php occ config:app:set oidc refresh_expire_time --value "86400" - ``` - -### MCP Server Deployment (Zero-config) -1. Set `NEXTCLOUD_HOST` environment variable -2. Set `NEXTCLOUD_MCP_SERVER_URL` (if not localhost:8000) -3. Start MCP server → Auto-registers on first run -4. Client credentials stored in `.nextcloud_oauth_client.json` - -### Alternative: Pre-registered Client -```bash -# Create client via CLI -php occ oidc:create \ - --name="Nextcloud MCP Server" \ - --type=confidential \ - --redirect-uri="http://localhost:8000/oauth/callback" - -# Set credentials in environment -NEXTCLOUD_OIDC_CLIENT_ID= -NEXTCLOUD_OIDC_CLIENT_SECRET= -``` - -## Testing Strategy - -### Unit Tests -- Token validation with mocked JWKS -- JWT claim extraction -- Client registration flow -- Bearer auth implementation - -### Integration Tests -- Dynamic client registration against test Nextcloud -- OAuth flow end-to-end -- Token-based API calls -- Client expiration and re-registration -- Dual-mode authentication (OAuth + BasicAuth) - -### Test Fixtures -```python -# tests/conftest.py additions: -@pytest.fixture -def mock_oidc_server(): - """Mock Nextcloud OIDC endpoints""" - # Mock /apps/oidc/openid-configuration - # Mock /apps/oidc/jwks - # Mock /apps/oidc/register - # Mock /apps/oidc/token - -@pytest.fixture -async def oauth_nc_client(mock_oidc_server): - """NextcloudClient with OAuth token""" - token = generate_test_jwt() - return NextcloudClient.from_token(base_url, token, "testuser") -``` - -## Migration Path - -### Phase 1: Implementation (Week 1-2) -- [ ] Implement token verifier with JWT validation -- [ ] Implement dynamic client registration -- [ ] Add BearerAuth for httpx -- [ ] Modify NextcloudClient for dual-mode auth -- [ ] Update app.py with OAuth configuration -- [ ] Add configuration management - -### Phase 2: Testing (Week 2-3) -- [ ] Unit tests for all auth components -- [ ] Integration tests with test Nextcloud instance -- [ ] End-to-end OAuth flow testing -- [ ] Backward compatibility testing - -### Phase 3: Documentation (Week 3) -- [ ] Update README.md with OAuth setup -- [ ] Update CLAUDE.md with architecture changes -- [ ] Add OAuth troubleshooting guide -- [ ] Document OIDC app configuration -- [ ] Add migration guide for existing deployments - -### Phase 4: Deployment (Week 4) -- [ ] Release with both modes supported -- [ ] Monitor for issues -- [ ] Deprecation notice for BasicAuth -- [ ] Plan BasicAuth removal timeline (6+ months) - -## Security Considerations - -### Token Security -- Store client secrets securely (file permissions, secret managers) -- Validate JWT signatures against trusted JWKS -- Verify token claims (issuer, audience, expiration) -- Implement token refresh logic -- Rate limit token validation failures - -### Client Registration Security -- Nextcloud OIDC provides BruteForce protection -- Dynamic clients limited to 100 per instance -- Clients expire after 1 hour (configurable) -- Admin must explicitly enable dynamic registration - -### API Security -- Bearer tokens used for Nextcloud API calls -- Token scopes control access levels -- User context preserved in all API operations -- No credential storage in MCP server - -## Performance Considerations - -### JWT Validation Performance -- JWKS caching with TTL (e.g., 1 hour) -- Key rotation handling via JWKS refresh -- Local validation (no network call per request) -- Async validation to avoid blocking - -### Client Creation -- OAuth mode: Per-request client creation (lightweight) -- BasicAuth mode: Single client in lifespan (current) -- Connection pooling maintained in both modes - -## Future Enhancements - -### Scope-based Authorization -- Define custom Nextcloud scopes for MCP operations -- Map MCP tools to required scopes -- Fine-grained permission control - -### Multi-tenant Support -- Support multiple Nextcloud instances -- Per-user client registration -- Tenant isolation - -### Token Introspection Fallback -- Implement RFC 7662 introspection -- Use if JWT validation fails -- Support for opaque tokens - -### Admin Controls -- MCP server admin UI for OAuth config -- Client credential rotation -- Usage monitoring and logging - -## Decisions Made (Post-Research) - -1. **✅ Token Validation Method**: Userinfo endpoint (primary), JWT optional - - Nextcloud OIDC does NOT provide introspection endpoint - - Userinfo endpoint validates token AND returns user claims - - JWT validation available as performance optimization if client configured - -2. **✅ Client expiration handling**: Auto re-register with logging - - Clients expire after 3600s by default - - Check expiry on startup and periodically - - Auto-register with backoff on failure - -3. **✅ Scope requirements**: `["openid", "profile", "email"]` - - Sufficient for basic user identification - - Optional: Add `"roles"` or `"groups"` for group-based authorization - -4. **✅ Token caching**: In-memory with 3600s TTL - - Cache userinfo response (includes all needed claims) - - Use token string as cache key - - TTL matches default access token lifetime - -5. **✅ Client storage**: JSON file with 0600 permissions - - Default: `.nextcloud_oauth_client.json` - - Configurable via env var - - Contains: client_id, client_secret, issued_at - -6. **✅ Username extraction**: From `sub` or `preferred_username` claim - - Both contain Nextcloud username (verified) - - Retrieved during token validation - - Cached with token - -7. **✅ BasicAuth deprecation**: 12 months after OAuth stable release - - Phase 1: OAuth + BasicAuth (6 months) - - Phase 2: OAuth only, deprecation warnings (6 months) - - Phase 3: Remove BasicAuth - -## Key Changes from Original Plan - -### 1. Token Validation -**Original**: JWT validation with JWKS (primary), introspection (fallback) -**Updated**: Userinfo endpoint (primary), JWT validation (optional optimization) -- Reason: Nextcloud OIDC has no introspection endpoint - -### 2. User Context Extraction -**Original**: Extract username from JWT claims -**Updated**: Fetch from userinfo endpoint during validation -- Reason: Opaque tokens by default, userinfo always works - -### 3. Token Caching Strategy -**Original**: MCP SDK handles all caching -**Updated**: Custom cache in TokenVerifier for userinfo responses -- Reason: Need to cache username separately from AccessToken - -### 4. JWT Support -**Original**: Required for all deployments -**Updated**: Optional performance optimization -- Reason: Requires per-client configuration in Nextcloud OIDC -- Default: Opaque tokens validated via userinfo - -## References - -- [MCP Python SDK OAuth Documentation](https://github.com/modelcontextprotocol/python-sdk) -- [MCP RFC 9728 Protected Resource Metadata](https://www.rfc-editor.org/rfc/rfc9728.html) -- [Nextcloud OIDC App Repository](https://github.com/H2CK/oidc) -- [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html) -- [RFC 9068 JWT Access Tokens](https://www.rfc-editor.org/rfc/rfc9068.html) -- [MCP Simple Auth Example](~/Software/python-sdk/examples/servers/simple-auth/) - -## Success Criteria - -✅ MCP server can authenticate via Nextcloud OIDC with zero manual client setup -✅ Dynamic client registration works automatically on first run -✅ JWT tokens validated locally without per-request network calls -✅ Backward compatibility maintained with BasicAuth mode -✅ All existing tests pass in both auth modes -✅ Documentation complete for OAuth setup and migration -✅ Security review passed (token handling, credential storage) -✅ Performance benchmarks meet targets (< 10ms token validation overhead) diff --git a/OAUTH_TESTING.md b/OAUTH_TESTING.md deleted file mode 100644 index d601866..0000000 --- a/OAUTH_TESTING.md +++ /dev/null @@ -1,121 +0,0 @@ -# OAuth Testing Setup - -This document describes the automated OAuth testing infrastructure for the Nextcloud MCP server. - -## Overview - -We've created a comprehensive testing setup that includes: - -1. **OIDC App Configuration** - Nextcloud OIDC app automatically installed and configured with dynamic client registration -2. **Dual MCP Services** - Two MCP server instances running in Docker: - - `mcp` (port 8000) - BasicAuth mode (username/password) - - `mcp-oauth` (port 8001) - OAuth mode (dynamic client registration) -3. **Test Fixtures** - Pytest fixtures for OAuth client testing -4. **Integration Tests** - OAuth-specific integration tests - -## Docker Compose Setup - -The `docker-compose.yml` includes: - -```yaml -services: - app: # Nextcloud with OIDC app enabled - mcp: # BasicAuth MCP server (port 8000) - mcp-oauth: # OAuth MCP server (port 8001) -``` - -## OIDC Configuration - -The OIDC app is configured automatically via `app-hooks/post-installation/install-oidc-app.sh`: - -- **Dynamic Client Registration**: Enabled -- **Config Key**: `dynamic_client_registration` (not `allow_dynamic_client_registration`) -- **Registration Endpoint**: `http://localhost:8080/apps/oidc/register` - -### Important: Config Key Fix - -The correct OIDC config key is `dynamic_client_registration`. The initial implementation used `allow_dynamic_client_registration` which was incorrect and caused the registration endpoint to not appear in the OIDC discovery document. - -## Test Fixtures - -Located in `tests/conftest.py`: - -### `oauth_token` -Session-scoped fixture that obtains an OAuth access token. - -**Current Limitation**: Nextcloud OIDC only supports `authorization_code` and `refresh_token` grant types, not the `password` grant type. This means we cannot automatically obtain tokens for testing without implementing a full browser-based OAuth flow. - -### `nc_oauth_client` -Session-scoped NextcloudClient configured with OAuth bearer token authentication. - -**Status**: Implemented but currently skipped due to token acquisition limitation. - -### `nc_mcp_oauth_client` -Session-scoped MCP client that connects to the OAuth-enabled MCP server on port 8001. - -**Status**: Implemented but marked as skip - requires full OAuth authorization flow implementation in MCP SDK. - -## Current Test Status - -### ✅ Working -- OIDC app installation and configuration -- Dynamic client registration -- OAuth infrastructure (BearerAuth, TokenVerifier, client registration) -- Docker compose dual-mode setup - -### ⚠️ Limitations -- **No automated token acquisition**: Nextcloud OIDC doesn't support the Resource Owner Password Credentials grant, which means we cannot programmatically get tokens for testing without browser interaction -- **Manual testing only**: OAuth functionality must be tested manually using a browser-based OAuth flow -- **MCP OAuth server untested**: The OAuth MCP server requires the full OAuth authorization flow to be implemented in the MCP Python SDK - -## Manual Testing OAuth - -To manually test OAuth functionality: - -1. Start the docker-compose environment: - ```bash - docker-compose up -d - ``` - -2. The OAuth MCP server runs on port 8001 and will: - - Automatically register a client via dynamic registration - - Store client credentials in `/app/.oauth/` volume - - Display OAuth configuration on startup - -3. To test OAuth with a real client: - - Use the authorization endpoint: `http://localhost:8080/apps/oidc/authorize` - - Implement the authorization code flow - - Exchange code for token at: `http://localhost:8080/apps/oidc/token` - -## Future Work - -To enable automated OAuth testing, one of these approaches is needed: - -1. **Mock OIDC Server**: Create a test OIDC server that supports password grant -2. **Browser Automation**: Use Selenium/Playwright to automate the OAuth flow -3. **Test-Only Password Grant**: Patch Nextcloud OIDC to support password grant in test mode -4. **Pre-generated Tokens**: Manually generate long-lived tokens and use them in tests - -## Running Tests - -```bash -# Run all tests (OAuth tests will be skipped) -uv run pytest tests/integration/test_oauth.py -v - -# Run only the invalid token test (this one works) -uv run pytest tests/integration/test_oauth.py::TestOAuthTokenValidation::test_invalid_token_fails -v -``` - -## Files Modified - -- `tests/conftest.py` - Added OAuth fixtures and token acquisition logic -- `tests/integration/test_oauth.py` - OAuth-specific integration tests -- `docker-compose.yml` - Added `mcp-oauth` service -- `app-hooks/post-installation/install-oidc-app.sh` - OIDC installation and configuration -- `nextcloud_mcp_server/client/__init__.py` - Added `from_token()` classmethod - -## Notes - -- The `from_token()` method was added to NextcloudClient to support OAuth authentication -- All OAuth infrastructure is in place and functional -- The main limitation is automated token acquisition for testing, not the OAuth implementation itself