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