Add OPENUI_OAUTH_FIX.md
This commit is contained in:
parent
610cf75c45
commit
21a99b2ff7
1 changed files with 218 additions and 0 deletions
218
OPENUI_OAUTH_FIX.md
Normal file
218
OPENUI_OAUTH_FIX.md
Normal file
|
|
@ -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.
|
||||||
Loading…
Reference in a new issue