Skip to content

fix(mcp): authorize JWT OAuth credential persistence - #41314

Merged
joshua-berri merged 11 commits into
mainfrom
litellm_fix_mcp_jwt_oauth_persistence
Sep 16, 2026
Merged

joshua-berri merged 11 commits into
mainfrom
litellm_fix_mcp_jwt_oauth_persistence

Conversation

@joshua-berri

@joshua-berri joshua-berri commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Gateway JWT clients could complete OAuth but fail MCP reconnects
  • Credential writes lacked target-server authorization
  • Unrelated browser bearers incorrectly blocked valid session cookies

How it solves it:

  • Reuse JWT identity, mapping, and permission resolution
  • Authorize credential storage before creating or replacing credentials
  • Preserve identity binding when storage is denied
  • Distinguish unrelated bearers from gateway credentials before browser authorization

User Flow

Before: a JWT-authenticated client completes upstream OAuth but cannot reliably reconnect

  1. The client authenticates to LiteLLM with its JWT
  2. The user completes upstream OAuth through POST /{server}/token
  3. The client reconnects to /mcp/{server} and receives 401
  4. An active credential could also save OAuth credentials for an unauthorized server

After: an authorized client can reconnect with persisted upstream credentials

  1. The client authenticates to LiteLLM with its JWT
  2. The user completes upstream OAuth through POST /{server}/token
  3. The client reconnects to /mcp/{server} and can list and call tools
  4. A caller without credential-management or server permission cannot create or replace the stored credentials

Relevant 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 uses authorize_jwt and the existing credential-management policy and server-access gate. Both share signature/custom validation, canonical user lookup, and existing authorization helpers

Normal auth_builder admission 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 behavior

Existing 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

  • I have added meaningful tests
  • Affected backend tests pass locally
  • Current-tip required CI/CD checks pass
  • Scope is isolated to OAuth credential ownership and authorization
  • Current-tip Greptile confidence is at least 4/5

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 commit

Changed 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 teams

Current-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 users

The repository's tests/e2e/mcp/oauth_chat_client.py supplied 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 requests

Working directory: /home/ubuntu/verify41314. Interpreter: /home/ubuntu/repos/litellm/.venv/bin/python. Saved browser state remained private. BASE is the corresponding proxy URL, port 4100 before and 4101 after; ALIAS is the isolated Linear server selected by the driver

The reconnect creates a fresh MCP client with only the gateway JWT, no OAuth provider or upstream token:

async with httpx.AsyncClient(headers=jwt_headers, timeout=30) as http:
    async with streamable_http_client(f"{BASE}/{ALIAS}/mcp", http_client=http) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            result = await session.call_tool(f"{ALIAS}-list_teams", {})

Before (171888b)

Gateway JWT in x-litellm-api-key

  1. Run 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
  2. Complete real Linear consent through the named server's /authorize and /token endpoints: an upstream access token is returned, but no credential row is stored
  3. Restart the proxy, PID 34707 to 53839. A fresh JWT-only client still receives 401; the credential remains absent

Gateway JWT in Authorization

  1. The same command executes the Authorization variant. Initial JWT-only MCP request: 401, no stored credential
  2. Complete the same real Linear consent and token exchange: an upstream access token is returned, but no credential row is stored
  3. Restart the proxy, PID 53839 to 54350. A fresh JWT-only client still receives 401; the credential remains absent

After (ee676d5)

Gateway JWT in x-litellm-api-key

  1. Run 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 credential
  2. Complete the same real Linear consent and token exchange: an upstream access token is returned and a credential row is stored for the correct user/server
  3. Restart the proxy, PID 50586 to 52586. A fresh JWT-only client lists 79 Linear tools and calls lineartipxk-list_teams: isError=false, result_present=true

Gateway JWT in Authorization

  1. Run 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 credential
  2. Complete the same real Linear consent and token exchange: an upstream access token is returned and a credential row is stored for the correct user/server
  3. Restart the proxy, PID 52586 to 53297. A fresh JWT-only client lists 79 Linear tools and calls lineartipaz-list_teams: isError=false, result_present=true

Both 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

  • Custom route policies must permit the credential-storage operation
  • Denied gateway credentials cannot fall back to browser cookies

Medium

  • Unscoped legacy JWT validators retain conservative bearer rejection
  • OIDC/custom-auth bearers remain gateway credential candidates

Low

  • Contributor-agreement confirmation remains outstanding

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@joshua-berri
joshua-berri requested a review from a team September 15, 2026 22:27
@codspeed

codspeed Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_mcp_jwt_oauth_persistence (ee676d5) with main (5960881)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (a8979fe) during the generation of this report, so 5960881 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@codecov

codecov Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens MCP OAuth credential persistence and separates identity binding from write authorization

  • Reuses JWT identity and authorization policy without provisioning users or teams during OAuth lookup
  • Applies route, centralized, and MCP server access checks immediately before credential persistence
  • Preserves credential-specific grants when constructing JWT authorization contexts
  • Allows confirmed unrelated browser bearers to defer to independently validated session cookies
  • Adds regression coverage for JWT validation, denied writes, browser sessions, mapped keys, team context, and database failures

Confidence Score: 5/5

The 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
Filename Overview
litellm/proxy/_experimental/mcp_server/bridge_token_flow.py Adds shared OAuth identity resolution, credential-write authorization, and conservative browser bearer classification
litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py Enforces credential policy during authorization and immediately before OAuth token persistence
litellm/proxy/_experimental/mcp_server/ui_session_utils.py Centralizes MCP server access across effective credential contexts
litellm/proxy/auth/handle_jwt.py Separates JWT identity resolution and authorization from provisioning-enabled admission while preserving grants
litellm/proxy/auth/user_api_key_auth.py Reuses the shared JWT result conversion for normal authentication
litellm/proxy/management_endpoints/mcp_management_endpoints.py Reuses centralized MCP server access evaluation
tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py Adds broad OAuth persistence, authorization, browser fallback, and failure-path regression coverage
tests/test_litellm/proxy/auth/test_handle_jwt.py Verifies identity and authorization paths do not provision while normal JWT admission retains provisioning

Reviews (10): Last reviewed commit: "fix(mcp): preserve browser OAuth for unr..." | Re-trigger Greptile

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

Please review the current tip for JWT credential ownership, authentication precedence, and OAuth persistence regressions

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/_experimental/mcp_server/bridge_token_flow.py Outdated

@ryan-crabbe-berri ryan-crabbe-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm; thats my only nit!

@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this commit for shared JWT resolver reuse, policy enforcement, canonical credential ownership, and regression coverage.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run — Please review the current commit for JWT resolver reuse, authentication policy, canonical credential ownership, and regression coverage.

Comment thread litellm/proxy/_experimental/mcp_server/bridge_token_flow.py Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/_experimental/mcp_server/bridge_token_flow.py
Comment thread litellm/proxy/_experimental/mcp_server/bridge_token_flow.py Outdated
@veria-ai

veria-ai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review the latest commit, including the resolved duplicate lookup, public OAuth identity mode, and admin credential attribution.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run Please verify the public-token route correction, unchanged MCP authorization, shared JWT identity lookup, and matching admin credential ownership.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review the merged current tip, preserving registered-agent validation alongside OAuth identity lookup and the corrected credential ownership checks.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run Please review the merged current tip, including registered-agent validation, public OAuth identity lookup, credential attribution, and normal MCP authorization.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/auth/handle_jwt.py Outdated
@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review the latest correction for admins without user rows, including missing-user versus database-failure handling and unchanged identity-only authorization boundaries.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run Please verify the confirmed admin-without-user-row issue is fixed, with ordinary missing users, inactive records, and database failures still rejected.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@joshua-berri joshua-berri changed the title fix(mcp): persist upstream OAuth credentials for JWT users fix(mcp): authorize JWT OAuth credential persistence Sep 16, 2026
@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review the updated credential-write authorization, including denied writes, JWT claim restrictions, identity binding, and shared policy reuse.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run Please review credential-write authorization, selected JWT team restrictions, denied overwrites, identity binding, and unchanged normal authentication defaults.

Comment thread litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please re-review c7e4160, especially shared signed-callback write policy and explicit JWT/key authorization before signed-code issuance.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run Please review c7e4160, including callback write authorization, explicit credential versus cookie precedence, and unchanged JWT admission behavior.

Comment thread litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py
@joshua-berri

Copy link
Copy Markdown
Contributor Author

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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review e035682, focusing on shared JWT authorization, provisioning isolation, and the existing OAuth credential-write policy

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run Please review e035682 for JWT provisioning isolation and OAuth authorization; browser precedence is unchanged and the compatibility finding remains open

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run Please review ee676d5 and confirm the unrelated-bearer cookie finding is resolved while denied gateway credentials remain blocked

@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review ee676d5 for unrelated-bearer cookie fallback, preserved credential rejection, shared identity resolution, and the added security regressions

Comment thread litellm/proxy/_experimental/mcp_server/bridge_token_flow.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please reassess ee676d5 using the before/after lookup comparison in discussion_r4023003683; the shared resolver replaces an existing browser-authorization database lookup

@joshua-berri
joshua-berri merged commit 9cd7873 into main Sep 16, 2026
97 checks passed
@joshua-berri
joshua-berri deleted the litellm_fix_mcp_jwt_oauth_persistence branch September 16, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants