fix(mcp): authorize JWT OAuth credential persistence - #41314
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR hardens MCP OAuth credential persistence and separates identity binding from write authorization
Confidence Score: 5/5The PR appears safe to merge, with no outstanding findings after reassessing the resolved request-path lookup concern The browser authorization resolver replaces the existing cache and database-backed key lookup instead of adding another lookup, and the current code applies write policy and server access checks before persistence Important Files Changed
Reviews (10): Last reviewed commit: "fix(mcp): preserve browser OAuth for unr..." | Re-trigger Greptile |
|
bugbot run Please review the current tip for JWT credential ownership, authentication precedence, and OAuth persistence regressions |
ryan-crabbe-berri
left a comment
There was a problem hiding this comment.
lgtm; thats my only nit!
|
@greptileai Please review this commit for shared JWT resolver reuse, policy enforcement, canonical credential ownership, and regression coverage. |
|
bugbot run — Please review the current commit for JWT resolver reuse, authentication policy, canonical credential ownership, and regression coverage. |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
|
@greptileai Please review the latest commit, including the resolved duplicate lookup, public OAuth identity mode, and admin credential attribution. |
|
bugbot run Please verify the public-token route correction, unchanged MCP authorization, shared JWT identity lookup, and matching admin credential ownership. |
|
@greptileai Please review the merged current tip, preserving registered-agent validation alongside OAuth identity lookup and the corrected credential ownership checks. |
|
bugbot run Please review the merged current tip, including registered-agent validation, public OAuth identity lookup, credential attribution, and normal MCP authorization. |
|
@greptileai Please review the latest correction for admins without user rows, including missing-user versus database-failure handling and unchanged identity-only authorization boundaries. |
|
bugbot run Please verify the confirmed admin-without-user-row issue is fixed, with ordinary missing users, inactive records, and database failures still rejected. |
|
@greptileai Please review the updated credential-write authorization, including denied writes, JWT claim restrictions, identity binding, and shared policy reuse. |
|
bugbot run Please review credential-write authorization, selected JWT team restrictions, denied overwrites, identity binding, and unchanged normal authentication defaults. |
|
@greptileai Please re-review c7e4160, especially shared signed-callback write policy and explicit JWT/key authorization before signed-code issuance. |
|
bugbot run Please review c7e4160, including callback write authorization, explicit credential versus cookie precedence, and unchanged JWT admission behavior. |
|
bugbot run Please reassess c7e4160 with the documented identity-bound authorization contract and the evidence in discussion_r4021952684. The explicit credential must pass validation and authorization; accepting a cookie after denial would restore the demonstrated credential-to-cookie downgrade. Cookie-only authorization is tested and preserved. Please identify any remaining defect under this fail-closed precedence contract. |
|
@greptileai Please review e035682, focusing on shared JWT authorization, provisioning isolation, and the existing OAuth credential-write policy |
|
bugbot run Please review e035682 for JWT provisioning isolation and OAuth authorization; browser precedence is unchanged and the compatibility finding remains open |
|
bugbot run Please review ee676d5 and confirm the unrelated-bearer cookie finding is resolved while denied gateway credentials remain blocked |
|
@greptileai Please review ee676d5 for unrelated-bearer cookie fallback, preserved credential rejection, shared identity resolution, and the added security regressions |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ee676d5. Configure here.
|
@greptileai Please reassess ee676d5 using the before/after lookup comparison in discussion_r4023003683; the shared resolver replaces an existing browser-authorization database lookup |
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a JWT-authenticated client completes upstream OAuth but cannot reliably reconnect
POST /{server}/token/mcp/{server}and receives 401After: an authorized client can reconnect with persisted upstream credentials
POST /{server}/token/mcp/{server}and can list and call toolsRelevant issues
Affected release
Linear ticket
Resolves LIT-3794
Resolves LIT-3795
Implementation
OAuth identity binding now uses
JWTAuthManager.resolve_identity, which verifies the token and resolves its owner without granting write permission. Credential persistence usesauthorize_jwtand the existing credential-management policy and server-access gate. Both share signature/custom validation, canonical user lookup, and existing authorization helpersNormal
auth_builderadmission supplies configured provisioning explicitly. OAuth lookup and authorization cannot create users or teams or synchronize memberships. This removes the boolean modes and copied-handler workaround while preserving mapped-key permissions and normal admission behaviorExisting storage, encryption, and upstream credential resolution remain the owners of persistence and egress
Browser authorization now distinguishes unrelated bearers from gateway credential candidates before calling the existing authorization gate. Opaque tokens use the shared IdentityStore and encrypted-session decoder; only a confirmed absent key can use the browser cookie. Explicit LiteLLM headers, gateway keys and envelopes, encrypted credentials, and identity lookup failures cannot bypass authorization through a cookie
JWT routing reuses the existing claims reader and issuer configuration. Unrelated issuers permit cookie authentication only when the global JWT validator is issuer-scoped. Configured issuers and ambiguous tokens still require full gateway authorization. Cookie identities independently pass session validation and the existing server-access check
Pre-Submission checklist
Local validation
On
ee676d59f259dbb1283b741b7856aaf8559ee3cf, the affected backend tests passed: 1,509 passed, 2 existing database-dependent skips. Full local lint, formatting, test-tree, strict-rule, type-discipline, test-quality, basedpyright, and API-schema gates passed. The tested source manifest matches this commitChanged executable lines: 203/203 (100%) against the PR merge base. Touched-function branches: 327/416 (78.61%), including existing large authentication functions. Both new credential-selection helpers have complete line coverage and 16/16 branches covered
The Bugbot regression reproduced at
e035682ed17295b9c5f0a363e266e890de31061c: unrelated opaque/JWT bearers incorrectly returned access_denied for valid cookies or prevented login for missing/expired cookies. The matching cases now pass. Rejection controls cover denied/expired keys and JWTs, malformed tokens, issuer configuration, explicit headers, encrypted credentials, database failures, and different cookie versus bearer owners. Authorize creates no stored credentials, users, or teamsCurrent-tip CI: 90 passed, 1 expected skip. Bugbot resolved the original finding and reported no new issues. Greptile's fresh review is 5/5, with its database-query concern explicitly withdrawn after the before/after comparison showed one lookup on both paths. Veria reported no security issues, with zero annotations. All review threads are resolved
Codecov currently covers 203/203 changed executable lines; final processing of the uploaded reports remains pending
Independent controlled-server verification also passed the allowed/denied raw-key and mapped-JWT write controls, callback user/server isolation, browser fallback rules, and provisioning boundaries. Devin independently reran 811 focused regressions on this commit
Screenshots / Proof of Fix
Independent Devin verification used real Linear at
https://mcp.linear.app/mcp, the saved browser login, and source builds at the exact commits below. Each leg started with no stored credential for its isolated user/server and an observed JWT-only 401 challenge. Server settings:transport=http,auth_type=oauth2,oauth2_flow=authorization_code,per_server_oauth_discovery=true; the isolated test server allowed the test usersThe repository's
tests/e2e/mcp/oauth_chat_client.pysupplied the OAuth SDK client, discovery, dynamic client registration, PKCE, and browser consent. The small external driver adapted gateway JWT headers, recorded state, and restarted the owned proxy. Protected-resource metadata advertised the named server's authorization service. Gateway headers were restricted to gateway requestsWorking directory:
/home/ubuntu/verify41314. Interpreter:/home/ubuntu/repos/litellm/.venv/bin/python. Saved browser state remained private.BASEis the corresponding proxy URL, port 4100 before and 4101 after;ALIASis the isolated Linear server selected by the driverThe reconnect creates a fresh MCP client with only the gateway JWT, no OAuth provider or upstream token:
Before (171888b)
Gateway JWT in x-litellm-api-key
DISPLAY=:0 LINEAR_HEADFUL=1 /home/ubuntu/repos/litellm/.venv/bin/python linear_case.py base, which executes both header variants. Initial JWT-only MCP request: 401, no stored credential/authorizeand/tokenendpoints: an upstream access token is returned, but no credential row is storedGateway JWT in Authorization
After (ee676d5)
Gateway JWT in x-litellm-api-key
DISPLAY=:0 LINEAR_HEADFUL=1 /home/ubuntu/repos/litellm/.venv/bin/python linear_case.py tip x-litellm-api-key. Initial JWT-only MCP request: 401, no stored credentiallineartipxk-list_teams:isError=false,result_present=trueGateway JWT in Authorization
DISPLAY=:0 LINEAR_HEADFUL=1 /home/ubuntu/repos/litellm/.venv/bin/python linear_case.py tip authorization. Initial JWT-only MCP request: 401, no stored credentiallineartipaz-list_teams:isError=false,result_present=trueBoth running source paths and hashes matched their commits before and after restart. No gateway credential was observed on off-origin requests. Only result-presence booleans were retained for the read-only Linear calls
Verification limits: the SDK's original Authorization-seeding session replaces that header with the upstream token and receives 401 on its next MCP request. The measured Authorization success above uses a fresh client carrying the gateway JWT, as required by this persistence regression. Stored bytes were checked as non-plaintext and the existing encryption path was reused; a separate cryptographic round-trip test was not performed
Type
Bug Fix
Caveats
Severe
Medium
Low
Final Attestation