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
- Create a gateway with GitHub OAuth configured (e.g.
https://api.githubcopilot.com/mcp/).
- Log in as
user@example.com and authorize the gateway via /oauth/authorize/<gateway_id>.
→ Authorization succeeds. Token stored in oauth_tokens.
- 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
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
https://api.githubcopilot.com/mcp/).user@example.comand authorize the gateway via/oauth/authorize/<gateway_id>.→ Authorization succeeds. Token stored in
oauth_tokens.user2@example.comand 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.comtries 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_tokenstable has two competing unique constraints left over from a schemamigration that was not completed cleanly.
unique_gateway_usergateway_id, user_id94f64b8e282f(original, stale)uq_oauth_gateway_usergateway_id, app_user_email14ac971cee42+ ORM model (current, correct)The critical distinction is what
user_idholds. It is populated from the OAuth Appclient_id— not from a per-person GitHub user identity. Theclient_idis the same valuefor every ContextForge user who authorizes via the same OAuth App registration.
Migration
14ac971cee42correctly addedapp_user_emailto scope tokens per ContextForge userand created
uq_oauth_gateway_user (gateway_id, app_user_email), but it never dropped theold
unique_gateway_user (gateway_id, user_id)constraint. The stale constraint fires onevery INSERT for a second user because
gateway_id + client_idis always the same tuple.The application logic in
token_storage_service.pyand the ORM model indb.pyare bothcorrect — 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 constraintmcpgateway/alembic/versions/14ac971cee42_add_user_context_to_oauth_tokens.py— did not drop itmcpgateway/db.py:5284— ORM model is correct (usesapp_user_email)mcpgateway/services/token_storage_service.py:157— upsert logic is correctExpected 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 pergateway — 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