feat: forward per-user ci_ keys at /mcp, scoped to the owner (#323)#325
Conversation
…eIntel#323) The remote /mcp endpoint authenticated every caller as one shared identity (MCP_API_KEY). Now each request carries the caller's own ci_ key, resolved per-request from a task-local ContextVar and sent as a per-request header; the backend's existing validator scopes results to that owner. Chose forwarding over re-validating in the MCP service: keeps auth in one authority (auth.py untouched, off-limits), adds zero DB round-trips, zero schema change. Chose a ContextVar set in pure-ASGI middleware over mutating the shared httpx client's headers: the latter races under concurrency and leaks tenant data; the ContextVar is task-local so it cannot. Pure ASGI (not BaseHTTPMiddleware) because the latter breaks contextvar propagation to the handler task. Fails closed: missing/invalid credential -> 401, no fallback to a shared identity for data calls. MCP_AUTH_TOKEN retained as a non-widening admin path. Latency-neutral; same single backend validation as before.
|
@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughReplaces the single shared ChangesPer-request CI API key auth for /mcp
Sequence DiagramsequenceDiagram
participant MCP Client
participant MCPAuthMiddleware
participant set_request_api_key
participant api_get/api_post/api_delete
participant _get_auth_header
participant Backend API
MCP Client->>MCPAuthMiddleware: HTTPS request with Authorization: Bearer ci_xxx
MCPAuthMiddleware->>MCPAuthMiddleware: _extract_bearer(scope) → ci_xxx
MCPAuthMiddleware->>set_request_api_key: set ci_xxx in ContextVar
MCPAuthMiddleware->>api_get/api_post/api_delete: forward to inner ASGI app
api_get/api_post/api_delete->>_get_auth_header: read _request_api_key ContextVar
_get_auth_header-->>api_get/api_post/api_delete: Authorization: Bearer ci_xxx
api_get/api_post/api_delete->>Backend API: request with per-user auth header
Backend API-->>api_get/api_post/api_delete: scoped response
api_get/api_post/api_delete-->>MCP Client: JSON result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary
The remote
/mcpendpoint authenticated every caller as one shared identity (MCP_API_KEY), so all clients saw the same repos. Now each request carries the caller's ownci_key, resolved per-request from a task-localContextVar, and the backend's existing validator scopes results to that key's owner.Closes / implements
oci/decisions/2026-06-16-mcp-per-user-api-key-auth.md(Status: Accepted)feat/mcp-per-user-key-authWhat changed
MCP server (
mcp-server/...):api_client.py- identity is now per-request via a task-localContextVar; the sharedhttpx.AsyncClientis identity-free (no baked-inAuthorization). Per-requestAuthorization: Bearer <key>is merged on each call. Falls back to the configured key when no per-request key is set (stdio / admin).server.py- replaced theBaseHTTPMiddlewareshared-token gate with a pure-ASGIMCPAuthMiddleware: fails closed, forwardsci_keys into the request context, keepsMCP_AUTH_TOKENas a non-widening admin path, leaves/healthpublic, logskey_suffixonly.Tests:
tests/test_auth_forwarding.py- 14 tests: per-request header resolution, Bearer-space regression guard (PR feat: MCP Connect guide -- live key injection + connection test (OPE-168) #292), concurrent cross-tenant isolation, fail-closed 401s, identity-free shared client, non-widening admin path.ADR adherence
api_keys)auth.pyuntouched)Pipeline
/oci-pipelineran clean - NOT YET RUN (deferred to before merge this session at author's direction)/oci-defend7/7 passed - NOT YET RUN (deferred to before merge)cd mcp-server && pytest tests/ -v- 60 passed (14 new + 46 existing)Note: the Paired-Ship diff-explanation rep and the
/oci-pipeline+/oci-defendgates were intentionally deferred this session. Run them before merge.How to test
ci_key in the dashboard for user A; index a repo under A.mcp.opencodeintel.com/mcpwithAuthorization: Bearer ci_<A's key>; calllist_repositories./mcpwith no Authorization header, and with a non-ci_bearer token.Expected: A sees only A's repos, B sees only B's; missing/invalid credentials return 401;
/healthstays public. Revoking A's key rejects A's next call.Deployment notes
MCP_AUTH_TOKENandMCP_API_KEYalready configured)auth.py,startup_checks.py,bun.lockbuntouched)mcppackage version change (still>=1.25.0,<2.0.0)/api/v1/*contractsBehavior change to flag: remote
/mcpnow always requires auth (aci_key or the admin token). Previously, withMCP_AUTH_TOKENunset, the endpoint was open. This is the intended fail-closed posture.Tradeoff annotation (Paired-Ship Protocol)
auth.pyuntouched), adds zero DB round-trips, and needs zero schema change.ContextVarin pure-ASGI middleware over mutating the sharedhttpxclient's headers: the latter races under concurrency and leaks tenant data; aContextVaris task-local and cannot.Risk and rollback
list_repositories,search_code,get_codebase_dna, dependency/impact) - all scope by the forwarded identity.Summary by CodeRabbit
New Features
/mcpendpoint.Tests