# OAuth Architecture This document explains how OAuth2/OIDC authentication works in the Nextcloud MCP Server implementation. ## Overview The Nextcloud MCP Server acts as an **OAuth 2.0 Resource Server**, protecting access to Nextcloud resources. It relies on Nextcloud's OIDC Identity Provider for user authentication and token validation. ## Architecture Diagram ``` ┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ │ │ │ │ │ │ MCP Client │ │ MCP Server │ │ Nextcloud │ │ (Claude, │ │ (Resource │ │ Instance │ │ etc.) │ │ Server) │ │ │ │ │ │ │ │ │ └──────┬──────┘ └────────┬─────────┘ └────────┬────────┘ │ │ │ │ │ │ │ 1. Connect to MCP │ │ ├─────────────────────────────────>│ │ │ │ │ │ 2. Return auth settings │ │ │ (issuer_url, scopes) │ │ │<─────────────────────────────────┤ │ │ │ │ │ │ │ │ 3. Start OAuth flow (with PKCE) │ │ ├──────────────────────────────────┼────────────────────────────────────>│ │ │ /apps/oidc/authorize │ │ │ │ │ 4. User authenticates in browser│ │ │<─────────────────────────────────┼─────────────────────────────────────┤ │ │ │ │ 5. Authorization code (redirect)│ │ │<─────────────────────────────────┤ │ │ │ │ │ 6. Exchange code for token │ │ ├──────────────────────────────────┼────────────────────────────────────>│ │ │ /apps/oidc/token │ │ │ │ │ 7. Access token │ │ │<─────────────────────────────────┼─────────────────────────────────────┤ │ │ │ │ │ │ │ 8. API request with Bearer token│ │ ├─────────────────────────────────>│ │ │ Authorization: Bearer xxx │ │ │ │ │ │ │ 9. Validate token via userinfo │ │ ├────────────────────────────────────>│ │ │ /apps/oidc/userinfo │ │ │ │ │ │ 10. User info (token valid) │ │ │<────────────────────────────────────┤ │ │ │ │ │ 11. Nextcloud API request │ │ ├────────────────────────────────────>│ │ │ Authorization: Bearer xxx │ │ │ (Notes, Calendar, etc.) │ │ │ │ │ │ 12. API response │ │ │<────────────────────────────────────┤ │ │ │ │ 13. MCP tool response │ │ │<─────────────────────────────────┤ │ │ │ │ ``` ## Components ### 1. MCP Client - Any MCP-compatible client (Claude Desktop, Claude Code, custom clients) - Initiates OAuth flow with PKCE (Proof Key for Code Exchange) - Stores and sends access token with each request - **Example**: Claude Desktop, Claude Code ### 2. MCP Server (Resource Server) - **Role**: OAuth 2.0 Resource Server - **Location**: This Nextcloud MCP Server implementation - **Responsibilities**: - Validates Bearer tokens by calling Nextcloud's userinfo endpoint - Caches validated tokens (default: 1 hour TTL) - Creates authenticated Nextcloud client instances per-user - Enforces PKCE requirements (S256 code challenge method) - Exposes Nextcloud functionality via MCP tools **Key Files**: - [`app.py`](../nextcloud_mcp_server/app.py) - OAuth mode detection and configuration - [`auth/token_verifier.py`](../nextcloud_mcp_server/auth/token_verifier.py) - Token validation logic - [`auth/context_helper.py`](../nextcloud_mcp_server/auth/context_helper.py) - Per-user client creation ### 3. Nextcloud OIDC Apps #### a) `oidc` - OIDC Identity Provider - **Role**: OAuth 2.0 Authorization Server - **Location**: Nextcloud app (`apps/oidc`) - **Endpoints**: - `/.well-known/openid-configuration` - Discovery endpoint - `/apps/oidc/authorize` - Authorization endpoint - `/apps/oidc/token` - Token endpoint - `/apps/oidc/userinfo` - User info endpoint (token validation) - `/apps/oidc/jwks` - JSON Web Key Set - `/apps/oidc/register` - Dynamic client registration **Configuration**: ```bash # Enable dynamic client registration (optional) # Settings → OIDC → "Allow dynamic client registration" ``` #### b) `user_oidc` - OpenID Connect User Backend - **Role**: Bearer token validation middleware - **Location**: Nextcloud app (`apps/user_oidc`) - **Responsibilities**: - Validates Bearer tokens for Nextcloud API requests - Creates user sessions from valid Bearer tokens - Integrates with Nextcloud's authentication system **Configuration**: ```bash # Enable Bearer token validation (required) php occ config:system:set user_oidc oidc_provider_bearer_validation --value=true --type=boolean ``` > [!IMPORTANT] > The `user_oidc` app requires a patch to properly support Bearer token authentication for non-OCS endpoints. See [Upstream Status](oauth-upstream-status.md) for details. ### 4. Nextcloud Instance - **Role**: Resource Owner / API Provider - **Provides**: Notes, Calendar, Contacts, Deck, Files, etc. ## Authentication Flow ### Phase 1: OAuth Authorization (Steps 1-7) 1. **Client Connects**: MCP client connects to MCP server 2. **Auth Settings**: MCP server returns OAuth settings: ```json { "issuer_url": "https://nextcloud.example.com", "resource_server_url": "http://localhost:8000", "required_scopes": ["openid", "profile"] } ``` 3. **OAuth Flow**: Client initiates OAuth flow with PKCE - Generates `code_verifier` (random string) - Calculates `code_challenge` = SHA256(code_verifier) - Redirects user to `/apps/oidc/authorize` with `code_challenge` 4. **User Authentication**: User logs in to Nextcloud via browser 5. **Authorization Code**: Nextcloud redirects back with authorization code 6. **Token Exchange**: Client exchanges code for access token - Sends `code` + `code_verifier` to `/apps/oidc/token` - OIDC app validates PKCE challenge 7. **Access Token**: Client receives access token (JWT or opaque) ### Phase 2: API Access (Steps 8-13) 8. **API Request**: Client sends MCP request with Bearer token 9. **Token Validation**: MCP server validates token: - Checks cache (1-hour TTL by default) - If not cached, calls `/apps/oidc/userinfo` with Bearer token - Extracts username from `sub` or `preferred_username` claim 10. **User Info**: Nextcloud returns user info if token is valid 11. **Nextcloud API Call**: MCP server calls Nextcloud API on behalf of user - Creates `NextcloudClient` instance with Bearer token - User-specific permissions apply 12. **API Response**: Nextcloud returns data 13. **MCP Response**: MCP server returns formatted response to client ## Token Validation The MCP server validates tokens using the **userinfo endpoint approach**: ### Why Userinfo (vs JWT Validation)? **Advantages**: - Works with both JWT and opaque tokens - No need to manage JWKS rotation - Always up-to-date (respects token revocation) - Simpler implementation **Caching Strategy**: - Validated tokens cached for 1 hour (configurable) - Cache keyed by token string - Expired tokens re-validated automatically **Implementation**: See [`NextcloudTokenVerifier`](../nextcloud_mcp_server/auth/token_verifier.py) ## PKCE Requirement The MCP server **requires** PKCE with S256 code challenge method: 1. Server validates OIDC discovery advertises PKCE support 2. Checks for `code_challenge_methods_supported` field 3. Verifies `S256` is included in supported methods 4. Logs error if PKCE not properly advertised **Why PKCE?**: - Required by MCP specification - Protects against authorization code interception - Essential for public clients (desktop apps, CLI tools) **Implementation**: See [`validate_pkce_support()`](../nextcloud_mcp_server/app.py#L31-L93) ## Client Registration The MCP server supports two client registration modes: ### Automatic Registration (Dynamic Client Registration) ```bash # No client credentials needed NEXTCLOUD_HOST=https://nextcloud.example.com ``` **How it works**: 1. Server checks `/.well-known/openid-configuration` for `registration_endpoint` 2. Calls `/apps/oidc/register` to register a client on first startup 3. Saves credentials to `.nextcloud_oauth_client.json` 4. Reuses these credentials on subsequent startups 5. Re-registers only if credentials are missing or expired **Best for**: Development, testing, quick deployments ### Pre-configured Client ```bash # Manual client registration via CLI php occ oidc:create --name="MCP Server" --type=confidential --redirect-uri="http://localhost:8000/oauth/callback" # Configure MCP server NEXTCLOUD_HOST=https://nextcloud.example.com NEXTCLOUD_OIDC_CLIENT_ID=abc123 NEXTCLOUD_OIDC_CLIENT_SECRET=xyz789 ``` **Best for**: Production, long-running deployments ## Per-User Client Instances Each authenticated user gets their own `NextcloudClient` instance: ```python # From MCP context (contains validated token) client = get_client_from_context(ctx) # Creates NextcloudClient with: # - username: from token's 'sub' or 'preferred_username' claim # - auth: BearerAuth(token) ``` **Benefits**: - User-specific permissions - Audit trail (actions appear from correct user) - No shared credentials - Multi-user support **Implementation**: See [`get_client_from_context()`](../nextcloud_mcp_server/auth/context_helper.py) ## Security Considerations ### Token Storage - MCP client stores access token - MCP server does NOT store tokens (validates per-request) - Token validation results cached in-memory only ### PKCE Protection - Server validates PKCE is advertised - Client MUST use PKCE with S256 - Protects against authorization code interception ### Scopes - Base required scopes: `openid`, `profile`, `email` - App-specific scopes control access to individual Nextcloud apps - See [OAuth Scopes](#oauth-scopes) section for complete scope reference ### Token Validation - Every MCP request validates Bearer token - Cached for performance (1-hour default) - Calls userinfo endpoint for validation ## OAuth Scopes The Nextcloud MCP Server implements fine-grained OAuth scopes for each Nextcloud app integration. Scopes control which tools are visible and accessible to users based on their granted permissions. ### Scope-Based Access Control When using OAuth authentication: 1. **Dynamic Discovery**: The server automatically discovers all required scopes from `@require_scopes` decorators on MCP tools 2. **Tool Filtering**: Tools are dynamically filtered based on the user's token scopes - users only see tools they have permission to use 3. **Per-Tool Enforcement**: Each tool validates required scopes before execution, returning a 403 error if insufficient scopes are present ### Supported Scopes The server supports the following OAuth scopes, organized by Nextcloud app: #### Base OIDC Scopes - `openid` - OpenID Connect authentication (required) - `profile` - Access to user profile information (required) - `email` - Access to user email address (required) #### Notes App - `notes:read` - Read notes, search notes, get note attachments - `notes:write` - Create, update, append to, and delete notes #### Calendar App - `calendar:read` - List calendars, read events, search events - `calendar:write` - Create, update, and delete calendars and events #### Calendar Tasks (VTODO) - `todo:read` - List and read CalDAV tasks - `todo:write` - Create, update, and delete CalDAV tasks #### Contacts App - `contacts:read` - List address books and read contacts (CardDAV) - `contacts:write` - Create, update, and delete address books and contacts #### Cookbook App - `cookbook:read` - Read recipes, search recipes - `cookbook:write` - Create, update, and delete recipes #### Deck App - `deck:read` - List boards, stacks, cards, and labels - `deck:write` - Create, update, and delete boards, stacks, cards, and labels #### Tables App - `tables:read` - List tables and read rows - `tables:write` - Create, update, and delete rows in tables #### Files (WebDAV) - `files:read` - List files, read file contents, search files - `files:write` - Upload, update, move, copy, and delete files #### Sharing - `sharing:read` - List shares and read share information - `sharing:write` - Create, update, and delete shares ### Scope Discovery The MCP server provides scope discovery through two mechanisms: #### 1. Protected Resource Metadata (PRM) Endpoint ```bash # Query the PRM endpoint curl http://localhost:8000/.well-known/oauth-protected-resource/mcp # Response includes dynamically discovered scopes { "resource": "http://localhost:8000/mcp", "scopes_supported": ["openid", "profile", "email", "notes:read", ...], "authorization_servers": ["https://nextcloud.example.com"], "bearer_methods_supported": ["header"], "resource_signing_alg_values_supported": ["RS256"] } ``` The `scopes_supported` field is **dynamically generated** from all registered MCP tools, ensuring it always reflects the actual available scopes. #### 2. Scope Enforcement via Decorators Tools are decorated with `@require_scopes()` to declare their required permissions: ```python from nextcloud_mcp_server.auth import require_scopes @mcp.tool() @require_scopes("notes:read") async def nc_notes_get_note(ctx: Context, note_id: int): """Get a specific note by ID""" # Implementation ``` ### Client Registration Scopes During OAuth client registration (dynamic or manual), clients request a set of scopes that define the **maximum allowed** scopes for that client. The actual per-tool enforcement is handled separately via decorators. **Environment Variable**: ```bash NEXTCLOUD_OIDC_SCOPES="openid profile email notes:read notes:write calendar:read calendar:write ..." ``` **Default**: All supported scopes (recommended for development) > **Note**: Client registration scopes define the maximum permissions. The MCP server's PRM endpoint dynamically advertises the actual supported scopes based on registered tools. ### Step-Up Authorization The server supports OAuth step-up authorization (RFC 8693). If a user attempts to use a tool requiring scopes they don't have: 1. Tool returns `403 Forbidden` with `InsufficientScopeError` 2. Response includes `WWW-Authenticate` header listing missing scopes: ``` WWW-Authenticate: Bearer error="insufficient_scope", scope="notes:write", resource_metadata="..." ``` 3. Client can re-authorize with additional scopes ### Scope Validation All scope enforcement happens at two levels: 1. **Tool Visibility**: During `list_tools` requests, only tools matching the user's token scopes are returned 2. **Execution Time**: When calling a tool, the `@require_scopes` decorator validates the token has necessary scopes **Example**: ```python # User token has: ["openid", "profile", "email", "notes:read"] # They will see: 4 read-only notes tools # They will NOT see: 3 write notes tools (notes:write required) # Attempting to call a write tool returns 403 Forbidden ``` ## Configuration See [Configuration Guide](configuration.md) for all OAuth environment variables: | Variable | Purpose | |----------|---------| | `NEXTCLOUD_HOST` | Nextcloud instance URL | | `NEXTCLOUD_OIDC_CLIENT_ID` | Pre-configured client ID (optional) | | `NEXTCLOUD_OIDC_CLIENT_SECRET` | Pre-configured client secret (optional) | | `NEXTCLOUD_MCP_SERVER_URL` | MCP server URL for OAuth callbacks | | `NEXTCLOUD_OIDC_CLIENT_STORAGE` | Path for auto-registered credentials | ## Testing The integration test suite includes comprehensive OAuth testing: - **Automated tests** (Playwright): [`tests/client/test_oauth_playwright.py`](../tests/client/test_oauth_playwright.py) - **Fixtures**: [`tests/conftest.py`](../tests/conftest.py) Run OAuth tests: ```bash # Start OAuth-enabled MCP server docker-compose up --build -d mcp-oauth # Run automated tests uv run pytest tests/client/test_oauth_playwright.py --browser firefox -v ``` ## See Also - [OAuth Setup Guide](oauth-setup.md) - Configuration steps - [OAuth Quick Start](quickstart-oauth.md) - Get started quickly - [Upstream Status](oauth-upstream-status.md) - Required upstream patches - [OAuth Troubleshooting](oauth-troubleshooting.md) - Common issues - [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) - OAuth 2.0 Authorization Framework - [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636) - PKCE - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)