Skip to content

[BUG]: OAuth callback fails with UNIQUE constraint error when a second user authorizes the same gateway #5538

Description

@rakdutta

Summary

When a second ContextForge user tries to authorize a gateway that another user has already
authorized via OAuth (e.g. a shared GitHub Copilot MCP gateway), the OAuth callback returns
HTTP 400 with the error:

UNIQUE constraint failed: oauth_tokens.gateway_id, oauth_tokens.user_id
The first user's token is stored successfully. Every subsequent user is blocked from completing
OAuth authorization for that gateway.


Steps to Reproduce

  1. Create a gateway with GitHub OAuth configured (e.g. https://api.githubcopilot.com/mcp/).
  2. Log in as user@example.com and authorize the gateway via /oauth/authorize/<gateway_id>.
    → Authorization succeeds. Token stored in oauth_tokens.
  3. Log in as user2@example.com and authorize the same gateway.
    → OAuth callback fails with HTTP 400. Token is not stored.

Error Log

ERROR mcpgateway.routers.oauth_router - OAuth callback failed: Token storage failed:
(sqlite3.IntegrityError) UNIQUE constraint failed: oauth_tokens.gateway_id, oauth_tokens.user_id
[SQL: INSERT INTO oauth_tokens (id, gateway_id, user_id, app_user_email, ...) VALUES (?, ?, ?, ?, ...)]
[parameters: ('', '<gateway_id>', 'Ov23li373LN81kl4mZCX', 'user2@example.com', ...)]
After this failure, if user2@example.com tries to use a tool on the gateway they get:

RPC error: Tool invocation failed: Please authorize git-server first.
Visit /oauth/authorize/<gateway_id> to complete OAuth flow.

Root Cause

The oauth_tokens table has two competing unique constraints left over from a schema
migration that was not completed cleanly.

Constraint Columns Source
unique_gateway_user gateway_id, user_id Migration 94f64b8e282f (original, stale)
uq_oauth_gateway_user gateway_id, app_user_email Migration 14ac971cee42 + ORM model (current, correct)

The critical distinction is what user_id holds. It is populated from the OAuth App
client_id
— not from a per-person GitHub user identity. The client_id is the same value
for every ContextForge user who authorizes via the same OAuth App registration.

Migration 14ac971cee42 correctly added app_user_email to scope tokens per ContextForge user
and created uq_oauth_gateway_user (gateway_id, app_user_email), but it never dropped the
old unique_gateway_user (gateway_id, user_id) constraint
. The stale constraint fires on
every INSERT for a second user because gateway_id + client_id is always the same tuple.

The application logic in token_storage_service.py and the ORM model in db.py are both
correct — the problem is purely a stale database schema constraint.

Affected files:

  • mcpgateway/alembic/versions/94f64b8e282f_add_oauth_tokens_table.py (line 53) — created the stale constraint
  • mcpgateway/alembic/versions/14ac971cee42_add_user_context_to_oauth_tokens.py — did not drop it
  • mcpgateway/db.py:5284 — ORM model is correct (uses app_user_email)
  • mcpgateway/services/token_storage_service.py:157 — upsert logic is correct

Expected Behavior

Each ContextForge user (app_user_email) should be able to independently authorize any gateway.
The unique constraint should be (gateway_id, app_user_email) — one token per app user per
gateway — which is what the ORM model already defines.


Actual Behavior

Only the first ContextForge user to authorize a given gateway succeeds. All subsequent users
receive a 400 error during the OAuth callback and cannot use tools requiring that gateway's
OAuth token.

Environment

  • Database: SQLite

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecurityImproves securitytriageIssues / Features awaiting triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions