mcp-servers/mcp-gateway/OPENUI_OAUTH_FIX.md

5.9 KiB

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

Store OAuth clients in a file or database instead of RAM.

File: gateway-proxy/oauth_storage.py (to be created)

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:

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

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:

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

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:

# 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

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:

from .oauth_storage import load_oauth_clients, save_oauth_clients

Around line 52, replace:

# REGISTERED_CLIENTS: dict[str, dict] = {}
REGISTERED_CLIENTS = load_oauth_clients()

In oauth_register() function, after line 383:

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:

gateway-proxy:
  volumes:
    - gateway-data:/data

volumes:
  gateway-data:

Step 4: Set environment variable (optional)

In .env:

OAUTH_STORAGE_FILE=/data/oauth_clients.json

Step 5: Restart

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:

docker exec mcp-gateway cat /data/oauth_clients.json | jq '.'

Check gateway logs for registration:

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.