# 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

The complete OAuth flow includes server startup (with DCR), client discovery (with PRM), authorization (with PKCE), and API access phases:

```
═══════════════════════════════════════════════════════════════════════════════════
Phase 0: MCP Server Startup & Client Registration (DCR - RFC 7591)
═══════════════════════════════════════════════════════════════════════════════════

                                 ┌──────────────────┐                  ┌─────────────────┐
                                 │   MCP Server     │                  │   Nextcloud     │
                                 │   (Resource      │                  │  (OIDC Provider)│
                                 │    Server)       │                  │                 │
                                 └────────┬─────────┘                  └────────┬────────┘
                                          │                                     │
                                          │  0a. OIDC Discovery                 │
                                          ├────────────────────────────────────>│
                                          │  GET                                │
                                          |  /.well-known/openid-configuration  │
                                          │                                     │
                                          │  0b. Discovery response             │
                                          │<────────────────────────────────────┤
                                          │  {issuer, endpoints, PKCE methods}  │
                                          │                                     │
                                          │  0c. Register OAuth client (DCR)    │
                                          ├────────────────────────────────────>│
                                          │  POST /apps/oidc/register           │
                                          │  {client_name, redirect_uris,       │
                                          │   scopes, token_type}               │
                                          │                                     │
                                          │  0d. Client credentials             │
                                          │<────────────────────────────────────┤
                                          │  {client_id, client_secret}         │
                                          │  → Saved to SQLite database         │
                                          │                                     │
                                          │  ✓ Server ready for connections     │


═══════════════════════════════════════════════════════════════════════════════════
Phase 1: Client Connection & Discovery (PRM - RFC 9728)
═══════════════════════════════════════════════════════════════════════════════════

┌─────────────┐                  ┌──────────────────┐                  ┌─────────────────┐
│             │                  │   MCP Server     │                  │   Nextcloud     │
│ MCP Client  │                  │   (Resource      │                  │   Instance      │
│ (Claude)    │                  │    Server)       │                  │                 │
│             │                  │                  │                  │                 │
└──────┬──────┘                  └────────┬─────────┘                  └────────┬────────┘
       │                                  │                                     │
       │  1a. Connect to MCP              │                                     │
       ├─────────────────────────────────>│                                     │
       │                                  │                                     │
       │  1b. Return auth settings        │                                     │
       │<─────────────────────────────────┤                                     │
       │  {issuer_url, resource_url}      │                                     │
       │                                  │                                     │
       │  1c. PRM Discovery (RFC 9728)    │                                     │
       ├─────────────────────────────────>│                                     │
       │  GET /.well-known/oauth-         │                                     │
       │      protected-resource/mcp      │                                     │
       │                                  │                                     │
       │  1d. PRM response (scopes!)      │                                     │
       │<─────────────────────────────────┤                                     │
       │  {resource, scopes_supported,    │  ← Dynamically discovered from      │
       │   authorization_servers}         │    @require_scopes decorators       │
       │                                  │                                     │


═══════════════════════════════════════════════════════════════════════════════════
Phase 2: OAuth Authorization Flow (PKCE - RFC 7636)
═══════════════════════════════════════════════════════════════════════════════════

       │                                  │                                     │
       │  2a. Generate PKCE challenge     │                                     │
       │  code_verifier = random(43-128)  │                                     │
       │  code_challenge = SHA256(verif.) │                                     │
       │                                  │                                     │
       │  2b. Authorization request       │                                     │
       ├──────────────────────────────────┼────────────────────────────────────>│
       │  /apps/oidc/authorize?           │                                     │
       │    client_id=xxx                 │                                     │
       │    &code_challenge=abc...        │                                     │
       │    &code_challenge_method=S256   │                                     │
       │    &scope=openid notes.read ...  │                                     │
       │                                  │                                     │
       │  2c. User consent page           │                                     │
       │<─────────────────────────────────┼─────────────────────────────────────┤
       │  (Browser: Select scopes)        │                                     │
       │                                  │                                     │
       │  2d. User grants scopes          │                                     │
       ├──────────────────────────────────┼────────────────────────────────────>│
       │                                  │                                     │
       │  2e. Authorization code redirect │                                     │
       │<─────────────────────────────────┼─────────────────────────────────────┤
       │  callback?code=xyz123            │                                     │
       │                                  │                                     │
       │  2f. Exchange code for token     │                                     │
       ├──────────────────────────────────┼────────────────────────────────────>│
       │  POST /apps/oidc/token           │                                     │
       │  {code, code_verifier,           │  ← Validates PKCE challenge         │
       │   client_id, client_secret}      │                                     │
       │                                  │                                     │
       │  2g. Access token (JWT/opaque)   │                                     │
       │<─────────────────────────────────┼─────────────────────────────────────┤
       │  {access_token, token_type,      │                                     │
       │   scope: "openid notes.read...") │  ← User's granted scopes            │
       │                                  │                                     │


═══════════════════════════════════════════════════════════════════════════════════
Phase 3: MCP Tool Access (Scope-based Authorization)
═══════════════════════════════════════════════════════════════════════════════════

       │                                  │                                     │
       │  3a. list_tools request          │                                     │
       ├─────────────────────────────────>│                                     │
       │  Authorization: Bearer <token>   │                                     │
       │                                  │                                     │
       │                                  │  3b. Validate token                 │
       │                                  ├────────────────────────────────────>│
       │                                  │  GET /apps/oidc/userinfo            │
       │                                  │  Authorization: Bearer <token>      │
       │                                  │                                     │
       │                                  │  3c. Token valid + scopes           │
       │                                  │<────────────────────────────────────┤
       │                                  │  {sub, scopes, ...}                 │
       │                                  │  ← Cached for 1 hour                │
       │                                  │                                     │
       │  3d. Filtered tool list          │                                     │
       │<─────────────────────────────────┤  ← Only tools matching user's       │
       │  [tools matching token scopes]   │    token scopes (via @require_scopes)
       │                                  │                                     │
       │  3e. Call tool                   │                                     │
       ├─────────────────────────────────>│                                     │
       │  nc_notes_get_note(note_id=1)    │  ← @require_scopes("notes.read")    │
       │  Authorization: Bearer <token>   │                                     │
       │                                  │                                     │
       │                                  │  3f. Scope check PASSED             │
       │                                  │  ✓ Token has notes.read             │
       │                                  │                                     │
       │                                  │  3g. Nextcloud API call             │
       │                                  ├────────────────────────────────────>│
       │                                  │  GET /apps/notes/api/v1/notes/1     │
       │                                  │  Authorization: Bearer <token>      │
       │                                  │  ← user_oidc validates Bearer token │
       │                                  │                                     │
       │                                  │  3h. API response                   │
       │                                  │<────────────────────────────────────┤
       │                                  │  {id: 1, title: "Note", ...}        │
       │                                  │                                     │
       │  3i. MCP tool response           │                                     │
       │<─────────────────────────────────┤                                     │
       │  {note data}                     │                                     │
       │                                  │                                     │


═══════════════════════════════════════════════════════════════════════════════════
Insufficient Scope Example (Step-Up Authorization)
═══════════════════════════════════════════════════════════════════════════════════

       │  4a. Call write tool             │                                     │
       ├─────────────────────────────────>│                                     │
       │  nc_notes_create_note(...)       │  ← @require_scopes("notes.write")   │
       │  Authorization: Bearer <token>   │                                     │
       │                                  │                                     │
       │                                  │  4b. Scope check FAILED             │
       │                                  │  ✗ Token only has notes.read        │
       │                                  │                                     │
       │  4c. 403 Insufficient Scope      │                                     │
       │<─────────────────────────────────┤                                     │
       │  WWW-Authenticate: Bearer        │                                     │
       │    error="insufficient_scope",   │                                     │
       │    scope="notes.write",          │                                     │
       │    resource_metadata="..."       │                                     │
       │                                  │                                     │
       │  → Client can re-authorize with  │                                     │
       │    additional scopes (Step-Up)   │                                     │
       │                                  │                                     │
```

## Components

### 1. MCP Client (e.g., Claude Desktop, Claude Code)

**Capabilities**:
- Discovers OAuth configuration via MCP server
- Queries PRM endpoint for supported scopes
- Initiates OAuth flow with PKCE (Proof Key for Code Exchange)
- Stores and sends access token with each request
- Handles scope-based tool filtering
- Supports step-up authorization (re-auth for additional scopes)

**Examples**: Claude Desktop, Claude Code, MCP Inspector, custom MCP clients

### 2. MCP Server (Resource Server - This Implementation)

**Role**: OAuth 2.0 Resource Server (RFC 6749)

**Responsibilities**:

#### Startup Phase
- **OIDC Discovery**: Queries `/.well-known/openid-configuration` for OAuth endpoints
- **PKCE Validation**: Verifies server advertises S256 code challenge method
- **Dynamic Client Registration (DCR)**: Automatically registers OAuth client via `/apps/oidc/register` (RFC 7591)
  - Or loads pre-configured client credentials
  - Saves credentials to SQLite database
- **Tool Registration**: Loads all MCP tools with their `@require_scopes` decorators

#### Client Connection Phase
- **Auth Settings**: Returns OAuth issuer URL and resource URL
- **PRM Endpoint**: Exposes `/.well-known/oauth-protected-resource/mcp` (RFC 9728)
  - Dynamically discovers scopes from all registered tools
  - Returns `scopes_supported` list based on `@require_scopes` decorators

#### Request Processing Phase
- **Token Validation**: Validates Bearer tokens via Nextcloud userinfo endpoint
  - Supports both JWT and opaque tokens
  - Caches validation results (1-hour TTL)
  - Extracts user identity and granted scopes
- **Scope Enforcement**:
  - Filters `list_tools` based on user's token scopes
  - Validates scopes before executing each tool
  - Returns 403 with `WWW-Authenticate` header for insufficient scopes
- **Per-User Clients**: Creates authenticated `NextcloudClient` instance per user
  - Uses Bearer token for all Nextcloud API requests
  - User-specific permissions and audit trails

**Key Files**:
- [`app.py`](../nextcloud_mcp_server/app.py) - OAuth mode, DCR, PRM endpoint
- [`auth/token_verifier.py`](../nextcloud_mcp_server/auth/token_verifier.py) - Token validation (userinfo + introspection + JWT)
- [`auth/context_helper.py`](../nextcloud_mcp_server/auth/context_helper.py) - Per-user client creation
- [`auth/scope_authorization.py`](../nextcloud_mcp_server/auth/scope_authorization.py) - `@require_scopes` decorator, scope discovery
- [`auth/client_registration.py`](../nextcloud_mcp_server/auth/client_registration.py) - DCR implementation (RFC 7591)

### 3. Nextcloud OIDC Apps

#### a) `oidc` - OIDC Identity Provider

**Role**: OAuth 2.0 Authorization Server + OIDC Provider

**Location**: Nextcloud app (`apps/oidc`)

**Endpoints**:
- `/.well-known/openid-configuration` - OIDC Discovery (RFC 8414)
- `/apps/oidc/authorize` - Authorization endpoint (OAuth 2.0 + PKCE)
- `/apps/oidc/token` - Token endpoint (issues JWT or opaque tokens)
- `/apps/oidc/userinfo` - UserInfo endpoint (OIDC Core, used for token validation)
- `/apps/oidc/jwks` - JSON Web Key Set (for JWT signature verification)
- `/apps/oidc/register` - Dynamic Client Registration endpoint (RFC 7591)
- `/apps/oidc/introspect` - Token Introspection endpoint (RFC 7662, optional)

**Token Types**:
- **JWT tokens**: Self-contained tokens with embedded scopes, validated via JWKS or userinfo
- **Opaque tokens**: Random strings, validated via userinfo or introspection endpoint

**Configuration**:
```bash
# Enable dynamic client registration (recommended for development)
# Nextcloud Admin → Settings → OIDC → "Allow dynamic client registration"

# Enable token introspection (optional, for opaque token validation)
# Nextcloud Admin → Settings → OIDC → "Enable token introspection"
```

#### b) `user_oidc` - OpenID Connect User Backend

**Role**: Bearer token validation middleware for Nextcloud APIs

**Location**: Nextcloud app (`apps/user_oidc`)

**Responsibilities**:
- Intercepts Nextcloud API requests with `Authorization: Bearer` header
- Validates tokens against OIDC provider (`oidc` app)
- Creates authenticated user sessions
- Enforces user-specific permissions on API requests

**Configuration**:
```bash
# Enable Bearer token validation (required for OAuth mode)
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 (like Notes API, Calendar API). See [Upstream Status](oauth-upstream-status.md) for patch details and PR status.

### 4. Nextcloud Instance

**Role**: Resource Owner + API Provider

**APIs Exposed**:
- **Notes API**: `/apps/notes/api/v1/` - Note CRUD operations
- **Calendar (CalDAV)**: `/remote.php/dav/calendars/` - Events and todos
- **Contacts (CardDAV)**: `/remote.php/dav/addressbooks/` - Contact management
- **Cookbook API**: `/apps/cookbook/api/v1/` - Recipe management
- **Deck API**: `/apps/deck/api/v1.0/` - Kanban boards
- **Tables API**: `/apps/tables/api/2/` - Table row operations
- **WebDAV (Files)**: `/remote.php/dav/files/` - File operations
- **Sharing API**: `/ocs/v2.php/apps/files_sharing/api/v1/` - Share management

## Authentication Flow

The OAuth flow consists of four distinct phases (see diagram above for visual representation):

### Phase 0: MCP Server Startup (One-time Setup)

**Happens**: On MCP server first startup

**Steps**:
1. **OIDC Discovery** (`GET /.well-known/openid-configuration`)
   - MCP server queries Nextcloud for OAuth endpoints
   - Validates PKCE support (requires `S256` code challenge method)
   - Extracts endpoints: authorize, token, userinfo, jwks, register

2. **Dynamic Client Registration** (`POST /apps/oidc/register`)
   - If no pre-configured client credentials exist
   - MCP server registers itself as OAuth client (RFC 7591)
   - Provides: client name, redirect URIs, requested scopes, token type
   - Receives: `client_id`, `client_secret`
   - Saves credentials to SQLite database

3. **Tool Registration**
   - All MCP tools loaded with their `@require_scopes` decorators
   - Scope metadata stored for later discovery

**Result**: MCP server ready to accept client connections

### Phase 1: Client Discovery (Per MCP Client Connection)

**Happens**: When MCP client first connects

**Steps**:
1. **MCP Connection**
   - Client connects to MCP server
   - Server returns OAuth auth settings (issuer URL, resource URL)

2. **PRM Discovery** (`GET /.well-known/oauth-protected-resource/mcp`)
   - Client queries Protected Resource Metadata endpoint (RFC 9728)
   - Server **dynamically discovers** scopes from all registered tools
   - Returns: resource URL, `scopes_supported` list, authorization servers
   - Client now knows which scopes are available

**Result**: Client knows OAuth configuration and available scopes

### Phase 2: OAuth Authorization (PKCE Flow - RFC 7636)

**Happens**: User authorizes access

**Steps**:
1. **PKCE Challenge Generation** (Client-side)
   - Generate `code_verifier`: random 43-128 character string
   - Calculate `code_challenge`: `BASE64URL(SHA256(code_verifier))`

2. **Authorization Request** (`GET /apps/oidc/authorize`)
   - Client redirects user to Nextcloud consent page
   - Parameters:
     - `client_id`: OAuth client ID
     - `code_challenge`: SHA256 hash of verifier
     - `code_challenge_method`: `S256`
     - `scope`: Requested scopes (e.g., `openid notes.read notes.write`)
     - `redirect_uri`: MCP server callback URL

3. **User Consent**
   - User authenticates to Nextcloud (if not already logged in)
   - User reviews and approves/denies requested scopes
   - Can select subset of requested scopes

4. **Authorization Code**
   - Nextcloud redirects to `callback?code=xyz123`
   - Code is bound to PKCE challenge

5. **Token Exchange** (`POST /apps/oidc/token`)
   - Client sends:
     - Authorization `code`
     - `code_verifier` (proves possession of original challenge)
     - `client_id` and `client_secret`
   - Nextcloud validates PKCE challenge: `SHA256(code_verifier) == code_challenge`
   - Nextcloud issues access token

6. **Access Token Response**
   - Token type: JWT or opaque (configurable)
   - Contains user's **granted scopes** (may be subset of requested)
   - Client stores token for subsequent requests

**Result**: Client has valid access token with granted scopes

### Phase 3: MCP Tool Access (Scope-Based Authorization)

**Happens**: Every MCP tool invocation

**Steps**:

#### Tool Listing (`list_tools`)
1. **List Tools Request**
   - Client sends `list_tools` with `Authorization: Bearer <token>`

2. **Token Validation**
   - MCP server calls `/apps/oidc/userinfo` with Bearer token
   - Nextcloud returns user info including **granted scopes**
   - Result cached for 1 hour

3. **Dynamic Tool Filtering**
   - Server compares token scopes with each tool's `@require_scopes`
   - Only returns tools where user has all required scopes
   - Example: Token with `notes.read` sees 4 read tools, not 3 write tools

4. **Filtered Tool List**
   - Client receives only tools they can use

#### Tool Execution (e.g., `nc_notes_get_note`)
1. **Tool Call**
   - Client invokes tool with `Authorization: Bearer <token>`

2. **Scope Validation**
   - `@require_scopes` decorator extracts token scopes
   - Verifies token contains required scope (e.g., `notes.read`)
   - If missing → 403 with `WWW-Authenticate` header (step-up auth)
   - If present → continues execution

3. **Nextcloud API Call**
   - MCP server creates `NextcloudClient` with Bearer token
   - Calls Nextcloud API (e.g., `GET /apps/notes/api/v1/notes/1`)
   - `user_oidc` app validates Bearer token again
   - Request executes as authenticated user

4. **Response**
   - Nextcloud returns data
   - MCP server formats response
   - Returns to client

**Result**: User can only access tools and data they have permissions for

### Phase 4: Insufficient Scope Handling (Step-Up Authorization)

**Happens**: When user lacks required scopes

**Steps**:
1. **Tool Call with Insufficient Scopes**
   - User calls `nc_notes_create_note` (requires `notes.write`)
   - But token only has `notes.read`

2. **Scope Validation Fails**
   - `@require_scopes("notes.write")` decorator checks token
   - Finds `notes.write` missing

3. **403 Response with Challenge**
   - Returns `403 Forbidden`
   - Includes `WWW-Authenticate` header:
     ```
     Bearer error="insufficient_scope",
            scope="notes.write",
            resource_metadata="http://localhost:8000/.well-known/oauth-protected-resource/mcp"
     ```

4. **Client Re-Authorization** (Optional)
   - Client can initiate new OAuth flow requesting additional scopes
   - User re-consents with expanded permissions
   - New token includes both `notes.read` and `notes.write`

**Result**: User can dynamically upgrade permissions without full re-authentication

## 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 SQLite database
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

#### Semantic Search (Multi-App Vector Database)
- `semantic.read` - Query vector database, perform semantic search across all indexed Nextcloud apps (notes, calendar, deck, files, contacts)
- `semantic.write` - Enable/disable background vector synchronization, manage indexing settings

> **Note**: Semantic search scopes provide access to the vector database that indexes content across **all** Nextcloud apps. Unlike app-specific scopes (e.g., `notes.read`), semantic scopes grant cross-app search capabilities powered by background vector synchronization (ADR-007).

### 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 |

## 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)
