diff --git a/mcp-gateway/OPENUI_OAUTH_FIX.md b/mcp-gateway/OPENUI_OAUTH_FIX.md new file mode 100644 index 0000000..a175cfa --- /dev/null +++ b/mcp-gateway/OPENUI_OAUTH_FIX.md @@ -0,0 +1,218 @@ +# OpenUI OAuth "Client not registered" Fix + +## Problem + +When connecting MCP Gateway to Open-UI, you get: +``` +{"error":"invalid_client","error_description":"Client not registered."} +``` + +Even though it works fine in Claude.ai. + +## Root Cause + +Your gateway uses **in-memory OAuth client registration** (`REGISTERED_CLIENTS` dict). This means: + +1. Each client (Claude.ai, Open-UI, etc.) registers itself via `/oauth/register` +2. The registration is stored in RAM only +3. When the gateway restarts → all registrations are lost +4. Open-UI's client ID is no longer recognized during token exchange + +Additionally, there are **two separate OAuth flows** happening: +- Claude.ai uses one client ID +- Open-UI registers and gets a different client ID +- If the gateway restarts between registration and first use, Open-UI's ID is gone + +## Solution Options + +### Option A: Persistent OAuth Client Storage (Recommended) +Store OAuth clients in a file or database instead of RAM. + +**File**: `gateway-proxy/oauth_storage.py` (to be created) + +```python +import json +import os +from typing import dict + +OAUTH_STORAGE_FILE = os.environ.get("OAUTH_STORAGE_FILE", "/data/oauth_clients.json") + +def load_oauth_clients() -> dict: + """Load OAuth clients from persistent storage""" + if os.path.exists(OAUTH_STORAGE_FILE): + try: + with open(OAUTH_STORAGE_FILE, 'r') as f: + return json.load(f) + except Exception as e: + logger.error(f"Failed to load OAuth clients: {e}") + return {} + return {} + +def save_oauth_clients(clients: dict) -> None: + """Save OAuth clients to persistent storage""" + try: + os.makedirs(os.path.dirname(OAUTH_STORAGE_FILE), exist_ok=True) + with open(OAUTH_STORAGE_FILE, 'w') as f: + json.dump(clients, f, indent=2) + except Exception as e: + logger.error(f"Failed to save OAuth clients: {e}") + +def register_client(client_info: dict) -> None: + """Register a new OAuth client""" + clients = load_oauth_clients() + client_id = client_info["client_id"] + clients[client_id] = client_info + save_oauth_clients(clients) +``` + +**Changes to gateway_proxy.py:** +```python +# At startup, replace: +# REGISTERED_CLIENTS: dict[str, dict] = {} +# with: +from .oauth_storage import load_oauth_clients, save_oauth_clients +REGISTERED_CLIENTS = load_oauth_clients() + +# In oauth_register function, after adding client: +REGISTERED_CLIENTS[client_id] = client_info +save_oauth_clients(REGISTERED_CLIENTS) # Add this line +``` + +**docker-compose.yml update:** +```yaml +gateway-proxy: + volumes: + - gateway-data:/data # Persist OAuth clients +``` + +### Option B: Pre-register Known Clients (Quick Fix) +Hardcode known clients (Claude.ai, Open-UI) so they don't need dynamic registration. + +**In .env:** +```bash +# Pre-registered OAuth clients (JSON format) +OAUTH_CLIENTS='{"claude-app":{"client_id":"claude-app","client_secret":"YOUR_SECRET","client_name":"Claude.ai"},"openui":{"client_id":"openui","client_secret":"YOUR_SECRET","client_name":"Open-UI"}}' +``` + +**In gateway_proxy.py:** +```python +import json + +def load_oauth_clients() -> dict: + """Load pre-registered clients from env""" + clients_json = os.environ.get("OAUTH_CLIENTS", "{}") + try: + return json.loads(clients_json) + except: + return {} + +REGISTERED_CLIENTS = load_oauth_clients() +``` + +### Option C: Disable Client Validation (Development Only) +For testing, skip client ID validation in token endpoint: + +**In oauth_token function, around line 520:** +```python +# Change this check: +client_info = REGISTERED_CLIENTS.get(client_id) +if not client_info: + return JSONResponse({"error": "invalid_client"}, status_code=400) + +# To this (development only): +client_info = REGISTERED_CLIENTS.get(client_id) +if not client_info: + logger.warning(f"Client {client_id} not registered, allowing anyway (DEV MODE)") + # Allow unregistered clients for testing +``` + +## Recommended Implementation + +**Use Option A** (persistent storage) because: +- ✅ Works across gateway restarts +- ✅ Supports multiple clients (Claude.ai, Open-UI, others) +- ✅ Secure (OAuth flow still requires proper credentials) +- ✅ Production-ready +- ✅ No hardcoded secrets + +## Implementation Steps + +### Step 1: Create oauth_storage.py +Save to `gateway-proxy/oauth_storage.py` (content provided above) + +### Step 2: Update gateway_proxy.py + +Around line 27 (imports), add: +```python +from .oauth_storage import load_oauth_clients, save_oauth_clients +``` + +Around line 52, replace: +```python +# REGISTERED_CLIENTS: dict[str, dict] = {} +REGISTERED_CLIENTS = load_oauth_clients() +``` + +In `oauth_register()` function, after line 383: +```python +REGISTERED_CLIENTS[client_id] = client_info +save_oauth_clients(REGISTERED_CLIENTS) # ADD THIS LINE +``` + +### Step 3: Update docker-compose.yml + +Add volume to gateway-proxy service: +```yaml +gateway-proxy: + volumes: + - gateway-data:/data + +volumes: + gateway-data: +``` + +### Step 4: Set environment variable (optional) +In `.env`: +```bash +OAUTH_STORAGE_FILE=/data/oauth_clients.json +``` + +### Step 5: Restart +```bash +docker-compose down +docker-compose up -d +``` + +## Testing + +1. Register with Open-UI (should succeed) +2. Restart gateway: `docker-compose restart gateway-proxy` +3. Try to use Open-UI again (should work - client is persisted) +4. Check `/data/oauth_clients.json` file exists with registration + +## Debugging + +Check if clients are saved: +```bash +docker exec mcp-gateway cat /data/oauth_clients.json | jq '.' +``` + +Check gateway logs for registration: +```bash +docker logs mcp-gateway | grep "DCR:" +``` + +If you still get "Client not registered": +1. Verify oauth_storage.py is in gateway-proxy/ +2. Check that save_oauth_clients() is being called +3. Check file permissions on /data/ volume +4. Check docker logs for save errors + +## Alternative: Use Redis or Database + +For production with multiple gateway instances, consider storing OAuth clients in: +- Redis (fast, distributed) +- PostgreSQL (persistent, queryable) +- MongoDB (flexible schema) + +This ensures clients are available across all instances.