mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(mcp): let the bridge envelope report expires_in 0 at the jwt exp boundary
_finish_bridge_mint floored the reported expires_in at 1. Admission expires the envelope against the JWT's second-truncated exp, so when the mint lands in the same second that exp falls on (a sub-second upstream lifetime, for instance), the true remaining life is 0 and reporting 1 tells the client the bearer lives one second past the point admission already rejects it. Floor at 0 instead so the reported lifetime never overstates the exp; the value still cannot go negative. The regression pins the boundary directly: minting at now=100.25 with a 1s upstream token seals exp=101, and the reported expires_in is max(0, 101 - ceil(100.25)) = 0. Under the old floor of 1 it reads 1, so the test fails on that mutation. Also drops the unused mcp_server parameter from _prepare_bridge_mint; identity and key derivation there never referenced the server.
This commit is contained in:
parent
2f0ddc82f7
commit
4ba7221b7a
2 changed files with 32 additions and 3 deletions
|
|
@ -821,7 +821,7 @@ def _bridge_mint_error_response(error: _BridgeMintError) -> JSONResponse:
|
|||
)
|
||||
|
||||
|
||||
async def _prepare_bridge_mint(request: Request, mcp_server: MCPServer) -> "_BridgeMintReady | _BridgeMintError":
|
||||
async def _prepare_bridge_mint(request: Request) -> "_BridgeMintReady | _BridgeMintError":
|
||||
"""Phase 1, BEFORE the upstream exchange: validate that the gateway can mint (master_key set) and
|
||||
that the request carries a resolvable litellm identity, and derive the envelope keys. Returns a
|
||||
ready context or a failure value. Running before the exchange is what makes a missing master_key or
|
||||
|
|
@ -866,7 +866,7 @@ def _finish_bridge_mint(
|
|||
return "too_large"
|
||||
# Report expires_in from the JWT's own second-truncated exp, rounding the elapsed portion up, so the
|
||||
# client is never told the bearer lives past the point admission (which uses that exp) rejects it.
|
||||
expires_in = max(1, int(sealed.expires_at.timestamp()) - math.ceil(now.timestamp()))
|
||||
expires_in = max(0, int(sealed.expires_at.timestamp()) - math.ceil(now.timestamp()))
|
||||
body = {"access_token": sealed.token.get_secret_value(), "token_type": "Bearer", "expires_in": expires_in}
|
||||
return JSONResponse(body, headers=TOKEN_NO_CACHE_HEADERS)
|
||||
|
||||
|
|
@ -949,7 +949,7 @@ async def exchange_token_with_server(
|
|||
# phase 3. A failure here returns without ever touching the upstream credential.
|
||||
bridge_mint_ready: _BridgeMintReady | None = None
|
||||
if mcp_server.is_oauth_delegate and mcp_server.is_dcr_bridge:
|
||||
prepared = await _prepare_bridge_mint(request, mcp_server)
|
||||
prepared = await _prepare_bridge_mint(request)
|
||||
if not isinstance(prepared, _BridgeMintReady):
|
||||
return _bridge_mint_error_response(prepared)
|
||||
bridge_mint_ready = prepared
|
||||
|
|
|
|||
|
|
@ -4609,6 +4609,35 @@ async def test_bridge_reported_expires_in_does_not_overstate_jwt_exp():
|
|||
assert before + body["expires_in"] <= claims["exp"]
|
||||
|
||||
|
||||
def test_bridge_reported_expires_in_can_be_zero_at_jwt_exp_boundary():
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
_BridgeMintReady,
|
||||
_finish_bridge_mint,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.outbound_credentials.bridge_credentials import (
|
||||
envelope_keys_from_master_key,
|
||||
)
|
||||
from litellm.types.mcp import MCPAuth
|
||||
|
||||
ready = _BridgeMintReady(
|
||||
key_hash="hashed-litellm-key-77",
|
||||
keys=envelope_keys_from_master_key(_BRIDGE_MASTER_KEY),
|
||||
)
|
||||
response = _finish_bridge_mint(
|
||||
ready=ready,
|
||||
mcp_server=_bridge_server(auth_type=MCPAuth.oauth_delegate),
|
||||
token_response={"access_token": "UP", "expires_in": 1},
|
||||
now=datetime.fromtimestamp(100.25, tz=timezone.utc),
|
||||
)
|
||||
|
||||
assert isinstance(response, JSONResponse)
|
||||
assert json.loads(response.body)["expires_in"] == 0
|
||||
|
||||
|
||||
def test_bridge_grant_coerces_numeric_expires_in():
|
||||
"""expires_in from an IdP may be an int, a float (3600.0), or a numeric string ("3600"); coerce
|
||||
it to a positive int so the envelope TTL honors the real lifetime instead of dropping a non-int
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue