|
| 1 | +# ToolHive Remote MCP Server Authentication Analysis |
| 2 | + |
| 3 | +This document analyzes how ToolHive handles remote MCP server authentication and its compliance with the [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization). |
| 4 | + |
| 5 | +## Executive Summary |
| 6 | + |
| 7 | +ToolHive is **highly compliant** with the MCP authorization specification, implementing all required features including RFC 9728 (Protected Resource Metadata), RFC 8414 (Authorization Server Metadata), RFC 7591 (Dynamic Client Registration), and PKCE support. |
| 8 | + |
| 9 | +## Specification Compliance |
| 10 | + |
| 11 | +### ✅ Fully Compliant Features |
| 12 | + |
| 13 | +#### 1. WWW-Authenticate Header Handling |
| 14 | +- **Location**: [`pkg/auth/discovery/discovery.go:159-233`](../pkg/auth/discovery/discovery.go#L159) |
| 15 | +- Correctly parses `Bearer` authentication scheme |
| 16 | +- Extracts `realm` and `resource_metadata` parameters as per RFC 9728 |
| 17 | +- Handles error and error_description parameters |
| 18 | + |
| 19 | +#### 2. Protected Resource Metadata (RFC 9728) |
| 20 | +- **Location**: [`pkg/auth/discovery/discovery.go:531-593`](../pkg/auth/discovery/discovery.go#L531) |
| 21 | +- Fetches metadata from `resource_metadata` URL in WWW-Authenticate header |
| 22 | +- Validates HTTPS requirement (with localhost exception for development) |
| 23 | +- Verifies required `resource` field presence |
| 24 | +- Extracts and processes `authorization_servers` array |
| 25 | + |
| 26 | +#### 3. Authorization Server Discovery (RFC 8414) |
| 27 | +- **Location**: [`pkg/auth/discovery/discovery.go:595-621`](../pkg/auth/discovery/discovery.go#L595) |
| 28 | +- Validates each authorization server in metadata |
| 29 | +- Discovers actual issuer via OIDC/.well-known endpoints |
| 30 | +- Handles issuer mismatch cases where metadata URL differs from actual issuer |
| 31 | +- Accepts the authoritative issuer from well-known endpoints per RFC 8414 |
| 32 | + |
| 33 | +#### 4. Dynamic Client Registration (RFC 7591) |
| 34 | +- **Location**: [`pkg/auth/oauth/dynamic_registration.go:82-200`](../pkg/auth/oauth/dynamic_registration.go#L82) |
| 35 | +- Automatically registers OAuth clients when no credentials provided |
| 36 | +- Uses PKCE flow with `token_endpoint_auth_method: "none"` |
| 37 | +- Supports both manual client configuration and automatic registration |
| 38 | + |
| 39 | +#### 5. PKCE Support |
| 40 | +- **Location**: [`pkg/auth/oauth/dynamic_registration.go:52`](../pkg/auth/oauth/dynamic_registration.go#L52) |
| 41 | +- Enabled by default for enhanced security |
| 42 | +- Required for public clients as per OAuth 2.1 |
| 43 | + |
| 44 | +## Authentication Flow |
| 45 | + |
| 46 | +### Initial Detection |
| 47 | +When ToolHive connects to a remote MCP server ([`pkg/runner/remote_auth.go:27-87`](../pkg/runner/remote_auth.go#L27)): |
| 48 | + |
| 49 | +1. Makes test request to the remote server (GET, then optionally POST) |
| 50 | +2. Checks for 401 Unauthorized response with WWW-Authenticate header |
| 51 | +3. Parses authentication requirements from the header |
| 52 | + |
| 53 | +### Discovery Priority Chain |
| 54 | +ToolHive follows this priority order for discovering the OAuth issuer ([`pkg/runner/remote_auth.go:95-145`](../pkg/runner/remote_auth.go#L95)): |
| 55 | + |
| 56 | +1. **Configured Issuer**: Uses `--remote-auth-issuer` flag if provided |
| 57 | +2. **Realm-Derived**: Derives from `realm` parameter in WWW-Authenticate header (RFC 8414) |
| 58 | +3. **Resource Metadata**: Fetches from `resource_metadata` URL (RFC 9728) |
| 59 | +4. **Well-Known Discovery**: Probes server's well-known endpoints to discover actual issuer (handles issuer mismatch) |
| 60 | +5. **URL-Derived**: Falls back to deriving from the remote URL (last resort) |
| 61 | + |
| 62 | +### Authentication Branches |
| 63 | + |
| 64 | +```mermaid |
| 65 | +graph TD |
| 66 | + A[Remote MCP Server Request] --> B{401 Response?} |
| 67 | + B -->|No| C[No Authentication Required] |
| 68 | + B -->|Yes| D{WWW-Authenticate Header?} |
| 69 | + D -->|No| E[No Authentication Required] |
| 70 | + D -->|Yes| F{Parse Header} |
| 71 | + |
| 72 | + F --> G{Has Realm URL?} |
| 73 | + G -->|Yes| H[Derive Issuer from Realm] |
| 74 | + H --> I[OIDC Discovery] |
| 75 | + |
| 76 | + F --> J{Has resource_metadata?} |
| 77 | + J -->|Yes| K[Fetch Resource Metadata] |
| 78 | + K --> L[Validate Auth Servers] |
| 79 | + L --> M[Use First Valid Server] |
| 80 | + |
| 81 | + F --> S{No Realm/Metadata?} |
| 82 | + S -->|Yes| T[Probe Well-Known Endpoints] |
| 83 | + T --> U{Found Valid Issuer?} |
| 84 | + U -->|Yes| V[Use Discovered Issuer] |
| 85 | + U -->|No| W[Derive from URL] |
| 86 | + |
| 87 | + I --> N{Client Credentials?} |
| 88 | + M --> N |
| 89 | + V --> N |
| 90 | + W --> N |
| 91 | + N -->|No| O[Dynamic Registration] |
| 92 | + N -->|Yes| P[OAuth Flow] |
| 93 | + O --> P |
| 94 | + |
| 95 | + P --> Q[Get Access Token] |
| 96 | + Q --> R[Authenticated Request] |
| 97 | +``` |
| 98 | + |
| 99 | +## Realm Handling |
| 100 | + |
| 101 | +When the server advertises a realm ([`pkg/auth/discovery/discovery.go:316-345`](../pkg/auth/discovery/discovery.go#L316)): |
| 102 | + |
| 103 | +1. Validates realm as HTTPS URL (RFC 8414 requirement) |
| 104 | +2. Strips query and fragment components to create valid issuer |
| 105 | +3. Uses as OAuth issuer for endpoint discovery |
| 106 | + |
| 107 | +Example: |
| 108 | +- Realm: `https://auth.example.com/realm/mcp?param=value#fragment` |
| 109 | +- Derived Issuer: `https://auth.example.com/realm/mcp` |
| 110 | + |
| 111 | +## Resource Metadata Processing |
| 112 | + |
| 113 | +When `resource_metadata` URL is provided: |
| 114 | + |
| 115 | +1. **Fetch Metadata**: GET request to the URL with JSON accept header |
| 116 | +2. **Validate Response**: Ensures HTTPS, checks content-type, validates `resource` field |
| 117 | +3. **Process Authorization Servers**: |
| 118 | + - Iterates through `authorization_servers` array |
| 119 | + - Validates each server via OIDC discovery |
| 120 | + - Uses first valid server found |
| 121 | +4. **Handle Issuer Mismatch**: Supports cases where metadata URL differs from actual issuer |
| 122 | + |
| 123 | +## Well-Known Endpoint Discovery |
| 124 | + |
| 125 | +When no realm URL or resource metadata is provided ([`pkg/runner/remote_auth.go:175-211`](../pkg/runner/remote_auth.go#L175)): |
| 126 | + |
| 127 | +1. **Derive Base URL**: Creates a base URL from the server URL |
| 128 | +2. **Probe Well-Known Endpoints**: Attempts to fetch OAuth metadata without requiring issuer match |
| 129 | +3. **Accept Authoritative Issuer**: Uses the issuer from the well-known response as authoritative per RFC 8414 |
| 130 | +4. **Log Mismatch**: Records when discovered issuer differs from server URL for debugging |
| 131 | + |
| 132 | +This approach handles cases where the OAuth provider's issuer differs from the server's public URL, such as when using CDN or worker deployments. |
| 133 | + |
| 134 | +## Dynamic Client Registration Flow |
| 135 | + |
| 136 | +When no client credentials are provided ([`pkg/auth/oauth/dynamic_registration.go`](../pkg/auth/oauth/dynamic_registration.go)): |
| 137 | + |
| 138 | +1. **Discover Registration Endpoint**: Via OIDC discovery or resource metadata |
| 139 | +2. **Create Registration Request**: |
| 140 | + ```json |
| 141 | + { |
| 142 | + "client_name": "ToolHive MCP Client", |
| 143 | + "redirect_uris": ["http://localhost:8765/callback"], |
| 144 | + "token_endpoint_auth_method": "none", |
| 145 | + "grant_types": ["authorization_code"], |
| 146 | + "response_types": ["code"] |
| 147 | + } |
| 148 | + ``` |
| 149 | +3. **Register Client**: POST to registration endpoint |
| 150 | +4. **Store Credentials**: Use returned client_id (and client_secret if provided) |
| 151 | +5. **Proceed with OAuth Flow**: Using registered credentials |
| 152 | + |
| 153 | +## Security Features |
| 154 | + |
| 155 | +### HTTPS Enforcement |
| 156 | +- All OAuth endpoints must use HTTPS |
| 157 | +- Exception for localhost/127.0.0.1 for development |
| 158 | +- Validates all discovered URLs |
| 159 | + |
| 160 | +### PKCE by Default |
| 161 | +- Automatically enabled for all OAuth flows |
| 162 | +- Required for public clients (no client_secret) |
| 163 | +- Provides protection against authorization code interception |
| 164 | + |
| 165 | +### Token Handling |
| 166 | +- Secure token storage in memory |
| 167 | +- Automatic token refresh support |
| 168 | +- Token passed via Authorization header to remote server |
| 169 | + |
| 170 | +### Configurable Timeouts |
| 171 | +- Authentication detection: 10 seconds default |
| 172 | +- OAuth flow: 5 minutes default |
| 173 | +- HTTP operations: 30 seconds default |
| 174 | + |
| 175 | +## Configuration Options |
| 176 | + |
| 177 | +### CLI Flags for Remote Authentication |
| 178 | + |
| 179 | +```bash |
| 180 | +# Automatic discovery (recommended) |
| 181 | +thv run https://remote-mcp-server.com |
| 182 | + |
| 183 | +# Manual OAuth configuration |
| 184 | +thv run https://remote-mcp-server.com \ |
| 185 | + --remote-auth-issuer https://auth.example.com \ |
| 186 | + --remote-auth-client-id my-client-id \ |
| 187 | + --remote-auth-client-secret my-secret \ |
| 188 | + --remote-auth-scopes "openid,profile,mcp" |
| 189 | + |
| 190 | +# Skip browser for headless environments |
| 191 | +thv run https://remote-mcp-server.com \ |
| 192 | + --remote-auth-skip-browser \ |
| 193 | + --remote-auth-timeout 2m |
| 194 | +``` |
| 195 | + |
| 196 | +### Registry Configuration |
| 197 | + |
| 198 | +Remote servers can be configured in the registry with OAuth settings: |
| 199 | + |
| 200 | +```json |
| 201 | +{ |
| 202 | + "version": "1.0.0", |
| 203 | + "last_updated": "2025-01-12T00:00:00Z", |
| 204 | + "remote_servers": { |
| 205 | + "example-remote": { |
| 206 | + "url": "https://remote-mcp-server.com", |
| 207 | + "description": "Remote MCP server with OAuth authentication", |
| 208 | + "tier": "community", |
| 209 | + "status": "active", |
| 210 | + "transport": "sse", |
| 211 | + "tools": ["tool1", "tool2"], |
| 212 | + "tags": ["remote", "oauth"], |
| 213 | + "headers": [ |
| 214 | + { |
| 215 | + "name": "X-API-Key", |
| 216 | + "description": "API key for authentication", |
| 217 | + "required": true, |
| 218 | + "secret": true |
| 219 | + } |
| 220 | + ], |
| 221 | + "oauth_config": { |
| 222 | + "issuer": "https://auth.example.com", |
| 223 | + "client_id": "optional-client-id", |
| 224 | + "scopes": ["openid", "profile", "mcp"], |
| 225 | + "callback_port": 8765, |
| 226 | + "use_pkce": true, |
| 227 | + "oauth_params": { |
| 228 | + "prompt": "consent" |
| 229 | + } |
| 230 | + } |
| 231 | + } |
| 232 | + } |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +The `oauth_config` section supports: |
| 237 | +- `issuer`: OIDC issuer URL for discovery |
| 238 | +- `authorize_url` & `token_url`: Manual OAuth endpoints (when not using OIDC) |
| 239 | +- `client_id`: Pre-configured client ID (optional, will use dynamic registration if not provided) |
| 240 | +- `scopes`: OAuth scopes to request |
| 241 | +- `callback_port`: Specific port for OAuth callback |
| 242 | +- `use_pkce`: Enable PKCE (defaults to true) |
| 243 | +- `oauth_params`: Additional OAuth parameters |
| 244 | + |
| 245 | +## Implementation Details |
| 246 | + |
| 247 | +### Key Components |
| 248 | + |
| 249 | +1. **RemoteAuthHandler** ([`pkg/runner/remote_auth.go`](../pkg/runner/remote_auth.go)) |
| 250 | + - Main entry point for remote authentication |
| 251 | + - Coordinates discovery and OAuth flow |
| 252 | + |
| 253 | +2. **Discovery Package** ([`pkg/auth/discovery/`](../pkg/auth/discovery/)) |
| 254 | + - WWW-Authenticate parsing |
| 255 | + - Resource metadata fetching |
| 256 | + - Authorization server validation |
| 257 | + |
| 258 | +3. **OAuth Package** ([`pkg/auth/oauth/`](../pkg/auth/oauth/)) |
| 259 | + - OIDC discovery |
| 260 | + - Dynamic client registration |
| 261 | + - OAuth flow execution with PKCE |
| 262 | + |
| 263 | +### Error Handling |
| 264 | + |
| 265 | +- Graceful fallback through discovery chain |
| 266 | +- Clear error messages for debugging |
| 267 | +- Retry logic for transient failures |
| 268 | +- Timeout protection for all operations |
| 269 | + |
| 270 | +## Compliance Summary |
| 271 | + |
| 272 | +| Specification | Status | Implementation | |
| 273 | +|--------------|--------|----------------| |
| 274 | +| RFC 9728 (Protected Resource Metadata) | ✅ Compliant | Full implementation with validation | |
| 275 | +| RFC 8414 (Authorization Server Metadata) | ✅ Compliant | Accepts authoritative issuer from well-known endpoints | |
| 276 | +| RFC 7591 (Dynamic Client Registration) | ✅ Compliant | Automatic registration when needed | |
| 277 | +| OAuth 2.1 PKCE | ✅ Compliant | Enabled by default | |
| 278 | +| WWW-Authenticate Parsing | ✅ Compliant | Supports Bearer with realm/resource_metadata | |
| 279 | +| Multiple Auth Servers | ✅ Compliant | Iterates and validates all servers | |
| 280 | +| Resource Parameter (RFC 8707) | ⚠️ Partial | Infrastructure ready, not yet sent in requests | |
| 281 | +| Token Audience Validation | ⚠️ Partial | Server-side validation support ready | |
| 282 | + |
| 283 | +## Future Enhancements |
| 284 | + |
| 285 | +While ToolHive is highly compliant with the current MCP specification, potential improvements include: |
| 286 | + |
| 287 | +1. **Resource Parameter**: Add explicit `resource` parameter to OAuth requests (infrastructure exists) |
| 288 | +2. **Token Audience Validation**: Enhanced client-side validation of token audience claims |
| 289 | +3. **Refresh Token Rotation**: Implement automatic refresh token rotation for long-lived sessions |
| 290 | +4. **Client Credential Caching**: Persist dynamically registered clients across sessions |
| 291 | + |
| 292 | +## Conclusion |
| 293 | + |
| 294 | +ToolHive's remote MCP server authentication implementation is comprehensive and standards-compliant, providing: |
| 295 | + |
| 296 | +- Full support for the MCP authorization specification |
| 297 | +- Automatic discovery and configuration |
| 298 | +- Dynamic client registration for zero-configuration setup |
| 299 | +- Strong security defaults with PKCE and HTTPS enforcement |
| 300 | +- Flexible configuration for various deployment scenarios |
| 301 | + |
| 302 | +The implementation correctly handles all specified authentication flows and provides a robust foundation for secure MCP server communication. |
0 commit comments