Skip to content

feat(mcp): identity-only session tokens for the gateway DCR front door - #33182

Merged
tin-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_lit3637_session_token
Jul 20, 2026
Merged

feat(mcp): identity-only session tokens for the gateway DCR front door#33182
tin-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_lit3637_session_token

Conversation

@tin-berri

@tin-berri tin-berri commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Stacked on #33174 (aggregate discovery front door); the base of this PR is litellm_lit3637_aggregate_dcr, not staging. Review and merge after #33174; the diff against that branch is the session-token module only

Linear ticket

Part of LIT-3637 (PR 2 of the stack: the identity-only session credential; pure module, unwired until the token endpoint and admission PRs land on top)

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

This is a pure, unwired module (nothing imports it yet), so the contract is pinned by unit tests rather than a live flow; the interactive proof lands with the token-endpoint and admission PRs stacked above, which will exercise these mints and openers on a live proxy end to end

pytest tests/test_litellm/proxy/_experimental/mcp_server/outbound_credentials/test_session_token.py \
       tests/test_litellm/proxy/_experimental/mcp_server/outbound_credentials/test_session_credentials.py -q
50 passed

Type

🆕 New Feature

Changes

PR 2 of the LIT-3637 aggregate DCR track: the client-held credential for the custody model. A DCR client that signs in through LiteLLM SSO holds ONE bearer carrying only a litellm identity; upstream tokens are vaulted server-side in LiteLLM_MCPUserCredentials and resolved by user at egress, so unlike the LIT-4338 bridge envelope there is nothing to seal. The token is a stable reference, not an authorization: admission (PR 4) reloads the live user record and policy on every request, so deactivating a user or their team kills outstanding sessions immediately without a revocation store. This is the same sealed-reference pattern the envelope admission arm settled on after security review of #32824

session_token.py is the pure module, mirroring envelope.py exactly: llm_session_ access and llm_srefresh_ refresh prefixes with a signed kind claim so a prefix swap cannot cross kinds; HS256 with a strict, extra="forbid" claims model as the sole type gate; PyJWT's iat/nbf/exp validators disabled for the reasons documented there (they raise on hostile claim types and compare against the wall clock rather than the injected clock); openers total over hostile input, returning tagged error values, never raising; 1h access and 14d refresh TTL caps matching the envelope bounds; a 4KB size cap enforced O(1)-cheaply before any JWT parsing. Claims are iss/iat/exp/kind/user_id/client_id; client_id binds the refresh token to the DCR client it was issued to per RFC 6749 section 6

session_credentials.py is the wired mirror of bridge_credentials.py: a memory-hard scrypt KDF from the proxy master key under a session-specific domain label, so session tokens and bridge envelopes never share key material (on top of distinct issuers, prefixes, and claim shapes); resolve_session_bearer for the admission edge (refresh tokens presented at the tool edge fail closed, expired is distinguished from tampered for logging only); open_session_refresh_bearer for the token endpoint with the client binding check inside

Tests cover round-trips and TTL caps, kind cross-replay via prefix swap, expiry boundary, tampered signatures, key rotation, alg=none rejection, signed-but-malformed claims (wrong issuer, coerced types, empty identity, extra nbf, missing claims), lone-surrogate and oversize hostile input including the multibyte char-vs-byte cap edge, the oversized-client_id mint guard, KDF domain separation from the envelope keys, Bearer-scheme stripping, and refresh client binding

QA runbook

Unit-only PR; run the two test files above. For a by-hand sanity check in a REPL: derive keys with session_keys_from_master_key("sk-1234"), mint with mint_session_token(SessionPrincipal(user_id="u", client_id="c"), keys, datetime.now(timezone.utc)), and confirm the minted value round-trips through resolve_session_bearer and fails closed after editing any character

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

Note

Medium Risk
New authentication token and KDF code with careful fail-closed design, but not yet wired into live admission; mistakes in follow-up PRs could affect gateway auth.

Overview
Adds a pure, unwired credential layer for the aggregate /mcp DCR custody model: client-held bearers carry only user_id and client_id, not upstream secrets.

session_token.py mints and validates llm_session_ / llm_srefresh_ HS256 JWTs (1h access, 14d refresh) with strict claims (kind, jti, issuer separation from bridge envelopes), a 4KB cap, and openers that return typed errors instead of raising on hostile input.

session_credentials.py derives signing keys from the proxy master_key via scrypt with a session-specific domain label (separate from envelope keys), plus resolve_session_bearer for MCP admission (refresh tokens fail closed at the tool edge) and open_session_refresh_bearer for token-endpoint refresh with client_id binding.

Unit tests cover round-trips, KDF separation, kind/prefix replay, expiry, tampering, and malformed JWT edge cases.

Reviewed by Cursor Bugbot for commit a22182f. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds the identity-only session token module for the gateway-level DCR front door (LIT-3637, PR 2 of the stack). It is a pure, unwired module that nothing imports yet — the token-endpoint and admission wiring land in later stacked PRs.

  • session_token.py mints and opens HS256 JWTs with llm_session_/llm_srefresh_ prefixes, a strict Pydantic claims gate (extra="forbid", strict=True), a signed kind claim to prevent cross-kind prefix-swap replay, and total error-value semantics over hostile input (size cap, surrogates, alg=none, tampered signatures all return typed error values rather than raising).
  • session_credentials.py derives the session signing key via a scrypt KDF under a domain label distinct from the bridge-envelope labels, exposes resolve_session_bearer for the admission edge (fails refresh tokens presented at the tool edge), and open_session_refresh_bearer for the token endpoint with RFC 6749 client-binding enforcement.
  • Tests cover round-trips, TTL boundaries, kind cross-replay, expiry, tamper, key rotation, alg=none, hostile inputs (surrogates, multibyte oversize, empty fields), and KDF domain separation from envelope keys — all without network calls.

Confidence Score: 5/5

Safe to merge. This is a self-contained, pure module with no call sites yet; its security properties are enforced by tests and the code itself is consistent with the existing envelope.py pattern.

The two new modules are pure, unwired additions with no side effects on any existing path. The token design is sound: KDF domain separation is verified, cross-kind replay is blocked at the signed-claims level, the Pydantic model is the strict total gate for every claim, and all error paths return values rather than raising. Tests cover all documented edge cases. No issues introduced into any existing code path.

No files require special attention. All four files are new additions with no modifications to existing code.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/outbound_credentials/session_token.py New pure module for identity-only session tokens: prefix routing, HS256 JWT with strict Pydantic gate, kind-claim cross-replay protection, and total error-value semantics over hostile input. Closely mirrors envelope.py; no issues found.
litellm/proxy/_experimental/mcp_server/outbound_credentials/session_credentials.py New KDF + edge/token-endpoint resolver layer; scrypt domain label distinct from bridge_credentials, lru_cache safe for a fixed process master key, resolver logic correctly fails closed on refresh-at-tool-edge and wrong-client-id. No issues found.
tests/test_litellm/proxy/_experimental/mcp_server/outbound_credentials/test_session_token.py Comprehensive new tests: round-trips, TTL boundary, kind cross-replay, tamper, key rotation, alg=none rejection, hostile input totality (surrogates, oversized, multibyte), and mint guard for oversized client_id. No network calls; all mocked.
tests/test_litellm/proxy/_experimental/mcp_server/outbound_credentials/test_session_credentials.py New tests covering KDF determinism, key length, domain separation from envelope keys, Bearer-scheme stripping for resolve_session_bearer, expired/tampered/wrong-key/wrong-client failure paths. No network calls.

Reviews (2): Last reviewed commit: "feat(mcp): add jti claim for per-mint se..." | Re-trigger Greptile

Comment on lines +177 to +179
iss: str
iat: int
exp: int

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.

P2 _SessionClaims.iss is typed as str rather than Literal["litellm-mcp-gateway"]. PyJWT's issuer=SESSION_ISSUER parameter is the primary enforcer, but if that check ever regresses (e.g., a future PyJWT API change silently skips issuer validation), the Pydantic model would pass any well-formed string in iss. Making it a Literal adds a second gate at zero cost.

Suggested change
iss: str
iat: int
exp: int
iss: Literal["litellm-mcp-gateway"]
iat: int
exp: int

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.41860% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...server/outbound_credentials/session_credentials.py 98.43% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@tin-berri
tin-berri force-pushed the litellm_lit3637_session_token branch from 186c862 to a22182f Compare July 19, 2026 02:16
Base automatically changed from litellm_lit3637_aggregate_dcr to litellm_internal_staging July 19, 2026 02:25
@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai rereview

@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 a22182f. Configure here.

@tin-berri
tin-berri merged commit ce88999 into litellm_internal_staging Jul 20, 2026
86 checks passed
@tin-berri
tin-berri deleted the litellm_lit3637_session_token branch July 20, 2026 18:24
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