mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(mcp): default the legacy KDF grace window on, force off under FIPS, cache the legacy derivation
legacy_kdf_grace_enabled required LITELLM_MCP_LEGACY_KDF_GRACE=true, so operators who never heard of the variable lost every main-era scrypt token on upgrade day. The window now defaults on and only explicit false-style values (false/0/no/off) disable it; FIPS deployments stay force-off via the existing is_fips_mode check. legacy_scrypt (n=2**15, memory-hard) is now lru_cache'd per process so repeated legacy-format token attempts derive the subkeys once instead of re-running scrypt per attempt.
This commit is contained in:
parent
48e3d911b5
commit
3d589dc35b
5 changed files with 218 additions and 38 deletions
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
Bridge envelopes and gateway session tokens are keyed by HKDF-SHA256 (RFC 5869) over the
|
||||
high-entropy proxy ``master_key`` with a per-family domain label as ``info``. The legacy
|
||||
scrypt derivation is kept for one release as an opt-in read fallback so tokens minted
|
||||
scrypt derivation is kept for one release as a default-on read fallback so tokens minted
|
||||
before the rotation still open during the grace window; it is never used under FIPS.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from typing import Final
|
||||
|
||||
from cryptography.hazmat.primitives.hashes import SHA256
|
||||
|
|
@ -18,6 +19,7 @@ from litellm.proxy.common_utils.fips import is_fips_mode
|
|||
|
||||
LEGACY_KDF_GRACE_ENV_VAR: Final = "LITELLM_MCP_LEGACY_KDF_GRACE"
|
||||
DERIVED_KEY_BYTES: Final = 32
|
||||
_LEGACY_GRACE_DISABLED_VALUES: Final = frozenset({"false", "0", "no", "off"})
|
||||
|
||||
# RFC 7914 work factors of the pre-rotation derivation; maxmem is twice the working set.
|
||||
_SCRYPT_N: Final = 2**15
|
||||
|
|
@ -31,9 +33,12 @@ def hkdf_sha256(master_key: str, info: bytes) -> str:
|
|||
return HKDF(algorithm=SHA256(), length=DERIVED_KEY_BYTES, salt=None, info=info).derive(master_key.encode()).hex()
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def legacy_scrypt(master_key: str, salt: bytes) -> str | None:
|
||||
"""The pre-rotation derivation. ``None`` when hashlib.scrypt is unavailable (some
|
||||
OpenSSL builds, including FIPS providers, do not expose it)."""
|
||||
OpenSSL builds, including FIPS providers, do not expose it). Cached per process: the
|
||||
work factors are memory-hard, so repeated legacy-format token attempts must not
|
||||
re-derive the same subkeys."""
|
||||
try:
|
||||
return hashlib.scrypt(
|
||||
master_key.encode(),
|
||||
|
|
@ -49,8 +54,11 @@ def legacy_scrypt(master_key: str, salt: bytes) -> str | None:
|
|||
|
||||
|
||||
def legacy_kdf_grace_enabled(environ: Callable[[str], str | None] = os.environ.get) -> bool:
|
||||
"""True only when the operator opted the legacy read fallback in and the deployment is
|
||||
not in FIPS mode (scrypt is not a FIPS-approved KDF)."""
|
||||
"""True unless the operator explicitly disabled the legacy read fallback and the
|
||||
deployment is not in FIPS mode (scrypt is not a FIPS-approved KDF). The window
|
||||
defaults on so main-era tokens survive the upgrade; only ``false``-style values
|
||||
(``false``/``0``/``no``/``off``) turn it off."""
|
||||
if is_fips_mode(environ):
|
||||
return False
|
||||
return (environ(LEGACY_KDF_GRACE_ENV_VAR) or "").strip().lower() == "true"
|
||||
raw: Final = environ(LEGACY_KDF_GRACE_ENV_VAR)
|
||||
return raw is None or raw.strip().lower() not in _LEGACY_GRACE_DISABLED_VALUES
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ ENVELOPE_SIGNING: Final = b"litellm-mcp-bridge:envelope-signing:"
|
|||
ENVELOPE_ENCRYPTION: Final = b"litellm-mcp-bridge:envelope-encryption:"
|
||||
SESSION_SIGNING: Final = b"litellm-mcp-gateway:session-signing:"
|
||||
GRACE: Final = {"LITELLM_MCP_LEGACY_KDF_GRACE": "true"}
|
||||
NO_GRACE: Final = {"LITELLM_MCP_LEGACY_KDF_GRACE": "false"}
|
||||
|
||||
|
||||
class _RefreshGrantForm(TypedDict):
|
||||
|
|
@ -239,14 +240,39 @@ def test_envelope_minted_under_hkdf_sha256_keys_is_admitted(gateway: Gateway) ->
|
|||
_assert_admitted(_tools_list(gateway.client, f"/{alias}/mcp", envelope), peer, upstream_token)
|
||||
|
||||
|
||||
def test_envelope_minted_under_legacy_scrypt_keys_is_rejected_without_grace(gateway: Gateway) -> None:
|
||||
with mcp_peer() as peer, oauth_server() as auth, gateway.scenario() as scenario:
|
||||
def test_envelope_minted_under_legacy_scrypt_keys_is_rejected_when_grace_is_explicitly_disabled(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with (
|
||||
owned_proxy(gateway, tmp_path, NO_GRACE) as strict,
|
||||
mcp_peer() as peer,
|
||||
oauth_server() as auth,
|
||||
strict.scenario() as scenario,
|
||||
):
|
||||
alias: Final = "kdf" + uuid.uuid4().hex[:8]
|
||||
identity: Final = _register_bridge(scenario, peer, alias, auth.issuer)
|
||||
key: Final = scenario.key(user_id=scenario.user(), object_permission=_ObjectPermission(mcp_servers=(identity,)))
|
||||
peer.drain()
|
||||
envelope: Final = _envelope(identity, key, _scrypt_envelope_keys(gateway.key), "up-" + uuid.uuid4().hex)
|
||||
_assert_rejected(_tools_list(gateway.client, f"/{alias}/mcp", envelope), peer)
|
||||
envelope: Final = _envelope(identity, key, _scrypt_envelope_keys(strict.key), "up-" + uuid.uuid4().hex)
|
||||
_assert_rejected(_tools_list(strict.client, f"/{alias}/mcp", envelope), peer)
|
||||
|
||||
|
||||
def test_envelope_minted_under_legacy_scrypt_keys_is_admitted_by_default_without_the_grace_variable(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with (
|
||||
owned_proxy(gateway, tmp_path, {}) as plain,
|
||||
mcp_peer() as peer,
|
||||
oauth_server() as auth,
|
||||
plain.scenario() as scenario,
|
||||
):
|
||||
alias: Final = "kdf" + uuid.uuid4().hex[:8]
|
||||
identity: Final = _register_bridge(scenario, peer, alias, auth.issuer)
|
||||
key: Final = scenario.key(user_id=scenario.user(), object_permission=_ObjectPermission(mcp_servers=(identity,)))
|
||||
upstream_token: Final = "up-" + uuid.uuid4().hex
|
||||
peer.drain()
|
||||
envelope: Final = _envelope(identity, key, _scrypt_envelope_keys(plain.key), upstream_token)
|
||||
_assert_admitted(_tools_list(plain.client, f"/{alias}/mcp", envelope), peer, upstream_token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kdf", ("hkdf", "scrypt"))
|
||||
|
|
@ -285,11 +311,11 @@ def test_envelope_under_a_foreign_master_key_is_rejected_during_grace(gateway: G
|
|||
_assert_rejected(_tools_list(graced.client, f"/{alias}/mcp", _envelope(identity, key, keys, "up")), peer)
|
||||
|
||||
|
||||
def test_grace_variable_set_to_anything_but_true_keeps_legacy_envelopes_rejected(
|
||||
def test_grace_variable_set_to_an_explicit_false_keeps_legacy_envelopes_rejected(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with (
|
||||
owned_proxy(gateway, tmp_path, {"LITELLM_MCP_LEGACY_KDF_GRACE": "yes"}) as proxy,
|
||||
owned_proxy(gateway, tmp_path, {"LITELLM_MCP_LEGACY_KDF_GRACE": "false"}) as proxy,
|
||||
mcp_peer() as peer,
|
||||
oauth_server() as auth,
|
||||
proxy.scenario() as scenario,
|
||||
|
|
@ -310,14 +336,26 @@ def test_session_token_minted_under_hkdf_sha256_key_is_admitted(gateway: Gateway
|
|||
assert "tools" in response.text, response.text
|
||||
|
||||
|
||||
def test_session_token_minted_under_legacy_scrypt_key_is_rejected_without_grace(gateway: Gateway) -> None:
|
||||
with gateway.scenario() as scenario:
|
||||
token: Final = _session(scenario.user(), _scrypt_session_keys(gateway.key))
|
||||
response: Final = _tools_list(gateway.client, "/mcp", token)
|
||||
def test_session_token_minted_under_legacy_scrypt_key_is_rejected_when_grace_is_explicitly_disabled(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with owned_proxy(gateway, tmp_path, NO_GRACE) as strict, strict.scenario() as scenario:
|
||||
token: Final = _session(scenario.user(), _scrypt_session_keys(strict.key))
|
||||
response: Final = _tools_list(strict.client, "/mcp", token)
|
||||
assert response.status_code == 401, response.text
|
||||
assert "invalid_token" in response.headers.get("www-authenticate", ""), response.headers
|
||||
|
||||
|
||||
def test_session_token_minted_under_legacy_scrypt_key_is_admitted_by_default_without_the_grace_variable(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with owned_proxy(gateway, tmp_path, {}) as plain, plain.scenario() as scenario:
|
||||
token: Final = _session(scenario.user(), _scrypt_session_keys(plain.key))
|
||||
response: Final = _tools_list(plain.client, "/mcp", token)
|
||||
assert response.status_code == 200, response.text
|
||||
assert "tools" in response.text, response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kdf", ("hkdf", "scrypt"))
|
||||
def test_session_token_of_either_kdf_is_admitted_during_the_legacy_grace_window(
|
||||
gateway: Gateway, tmp_path: Path, kdf: str
|
||||
|
|
@ -358,15 +396,21 @@ def test_bridge_refresh_envelope_minted_under_hkdf_sha256_keys_is_renewed(gatewa
|
|||
assert isinstance(opened, OpenedRefreshEnvelope), opened
|
||||
|
||||
|
||||
def test_bridge_refresh_envelope_minted_under_legacy_scrypt_keys_is_rejected_without_grace(
|
||||
def test_bridge_refresh_envelope_minted_under_legacy_scrypt_keys_is_rejected_when_grace_is_explicitly_disabled(
|
||||
gateway: Gateway,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with mcp_peer() as peer, oauth_server() as auth, gateway.scenario() as scenario:
|
||||
with (
|
||||
owned_proxy(gateway, tmp_path, NO_GRACE) as strict,
|
||||
mcp_peer() as peer,
|
||||
oauth_server() as auth,
|
||||
strict.scenario() as scenario,
|
||||
):
|
||||
alias: Final = "kdf" + uuid.uuid4().hex[:8]
|
||||
identity: Final = _register_bridge(scenario, peer, alias, auth.issuer)
|
||||
key: Final = scenario.key(user_id=scenario.user(), object_permission=_ObjectPermission(mcp_servers=(identity,)))
|
||||
envelope: Final = _refresh_envelope(identity, key, _scrypt_envelope_keys(gateway.key), _upstream_refresh(auth))
|
||||
response: Final = gateway.client.post(
|
||||
envelope: Final = _refresh_envelope(identity, key, _scrypt_envelope_keys(strict.key), _upstream_refresh(auth))
|
||||
response: Final = strict.client.post(
|
||||
f"/{alias}/token",
|
||||
headers=(("x-litellm-api-key", key),),
|
||||
data=_RefreshGrantForm(grant_type="refresh_token", refresh_token=envelope, client_id="kdf-client"),
|
||||
|
|
@ -430,11 +474,13 @@ def test_session_refresh_token_minted_under_hkdf_sha256_key_is_renewed(gateway:
|
|||
assert isinstance(opened, OpenedSessionToken), opened
|
||||
|
||||
|
||||
def test_session_refresh_token_minted_under_legacy_scrypt_key_is_rejected_without_grace(gateway: Gateway) -> None:
|
||||
with gateway.scenario() as scenario:
|
||||
client_id: Final = _register_gateway_client(gateway.client)
|
||||
refresh: Final = _session_refresh(scenario.user(), client_id, _scrypt_session_keys(gateway.key))
|
||||
response: Final = gateway.client.post(
|
||||
def test_session_refresh_token_minted_under_legacy_scrypt_key_is_rejected_when_grace_is_explicitly_disabled(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
with owned_proxy(gateway, tmp_path, NO_GRACE) as strict, strict.scenario() as scenario:
|
||||
client_id: Final = _register_gateway_client(strict.client)
|
||||
refresh: Final = _session_refresh(scenario.user(), client_id, _scrypt_session_keys(strict.key))
|
||||
response: Final = strict.client.post(
|
||||
"/token", data=_RefreshGrantForm(grant_type="refresh_token", refresh_token=refresh, client_id=client_id)
|
||||
)
|
||||
assert response.status_code == 400, response.text
|
||||
|
|
@ -536,7 +582,21 @@ def test_claude_code_gateway_refresh_of_an_hkdf_session_token_is_renewed_without
|
|||
assert isinstance(opened, OpenedSessionToken), opened
|
||||
|
||||
|
||||
def test_claude_code_gateway_refresh_of_a_legacy_scrypt_session_token_is_rejected_without_grace(
|
||||
def test_claude_code_gateway_refresh_of_a_legacy_scrypt_session_token_is_rejected_when_grace_is_explicitly_disabled(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
config: Final = _claude_code_gateway_config(tmp_path)
|
||||
with owned_proxy(gateway, tmp_path, NO_GRACE, config=config) as graced, graced.scenario() as scenario:
|
||||
refresh: Final = _proxy_api_session_refresh(scenario.user(), _scrypt_session_keys(graced.key))
|
||||
response: Final = graced.client.post(
|
||||
"/claude_code_gateway/oauth/token",
|
||||
data=_ClaudeCodeTokenForm(grant_type="refresh_token", refresh_token=refresh),
|
||||
)
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json()["error"] == "invalid_grant", response.text
|
||||
|
||||
|
||||
def test_claude_code_gateway_refresh_of_a_legacy_scrypt_session_token_is_renewed_by_default_without_the_grace_variable(
|
||||
gateway: Gateway, tmp_path: Path
|
||||
) -> None:
|
||||
config: Final = _claude_code_gateway_config(tmp_path)
|
||||
|
|
@ -546,8 +606,12 @@ def test_claude_code_gateway_refresh_of_a_legacy_scrypt_session_token_is_rejecte
|
|||
"/claude_code_gateway/oauth/token",
|
||||
data=_ClaudeCodeTokenForm(grant_type="refresh_token", refresh_token=refresh),
|
||||
)
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json()["error"] == "invalid_grant", response.text
|
||||
assert response.status_code == 200, response.text
|
||||
renewed: Final = response.json()
|
||||
opened: Final = open_session_refresh_token(
|
||||
renewed["refresh_token"], _hkdf_session_keys(graced.key), datetime.now(timezone.utc)
|
||||
)
|
||||
assert isinstance(opened, OpenedSessionToken), opened
|
||||
|
||||
|
||||
def test_claude_code_gateway_refresh_of_a_legacy_scrypt_session_token_is_renewed_during_the_legacy_grace_window(
|
||||
|
|
|
|||
|
|
@ -411,13 +411,29 @@ def test_legacy_envelope_opens_during_the_grace_window():
|
|||
assert result.identity == _IDENTITY
|
||||
|
||||
|
||||
def test_legacy_envelope_is_invalid_without_grace():
|
||||
def test_legacy_envelope_is_invalid_when_grace_is_explicitly_disabled():
|
||||
token: Final = _sealed_token(_legacy_keys(_MASTER_KEY))
|
||||
assert legacy_envelope_keys_from_master_key(_MASTER_KEY, environ=_environ(MappingProxyType({}))) is None
|
||||
assert (
|
||||
legacy_envelope_keys_from_master_key(
|
||||
_MASTER_KEY, environ=_environ(MappingProxyType({LEGACY_KDF_GRACE_ENV_VAR: "false"}))
|
||||
)
|
||||
is None
|
||||
)
|
||||
result: Final = resolve_bridge_envelope(token, envelope_keys_from_master_key(_MASTER_KEY), _NOW, _SERVER_ID)
|
||||
assert isinstance(result, BridgeEnvelopeInvalid)
|
||||
|
||||
|
||||
def test_legacy_envelope_opens_by_default_without_the_grace_variable():
|
||||
legacy: Final = _legacy_keys(_MASTER_KEY)
|
||||
token: Final = _sealed_token(legacy)
|
||||
grace: Final = legacy_envelope_keys_from_master_key(_MASTER_KEY, environ=_environ(MappingProxyType({})))
|
||||
assert grace == legacy
|
||||
result: Final = resolve_bridge_envelope(
|
||||
token, envelope_keys_from_master_key(_MASTER_KEY), _NOW, _SERVER_ID, legacy_keys=grace
|
||||
)
|
||||
assert isinstance(result, BridgeEnvelopeAdmitted)
|
||||
|
||||
|
||||
def test_grace_is_disabled_under_fips():
|
||||
grace: Final = legacy_envelope_keys_from_master_key(
|
||||
_MASTER_KEY,
|
||||
|
|
@ -426,13 +442,20 @@ def test_grace_is_disabled_under_fips():
|
|||
assert grace is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ("yes", "1", "on", "TRUE ", "True"))
|
||||
def test_grace_is_enabled_only_by_case_insensitive_true(value: str):
|
||||
@pytest.mark.parametrize("value", ("false", "0", "no", "off", "FALSE ", " Off"))
|
||||
def test_grace_is_disabled_only_by_explicit_false_style_values(value: str):
|
||||
result: Final = legacy_envelope_keys_from_master_key(
|
||||
_MASTER_KEY, environ=_environ(MappingProxyType({LEGACY_KDF_GRACE_ENV_VAR: value}))
|
||||
)
|
||||
expected: Final = value.strip().lower() == "true"
|
||||
assert (result is not None) is expected
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ("true", "1", "yes", "on", "TRUE ", "True", "", "banana"))
|
||||
def test_grace_stays_on_for_unset_true_style_and_unrecognized_values(value: str):
|
||||
result: Final = legacy_envelope_keys_from_master_key(
|
||||
_MASTER_KEY, environ=_environ(MappingProxyType({LEGACY_KDF_GRACE_ENV_VAR: value}))
|
||||
)
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_legacy_refresh_envelope_opens_during_grace():
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from litellm.proxy._experimental.mcp_server.outbound_credentials.bridge_credenti
|
|||
)
|
||||
from litellm.proxy._experimental.mcp_server.outbound_credentials.key_derivation import (
|
||||
LEGACY_KDF_GRACE_ENV_VAR,
|
||||
legacy_scrypt,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_credentials import (
|
||||
NotSessionBearer,
|
||||
|
|
@ -276,10 +277,15 @@ def test_legacy_session_token_opens_during_the_grace_window():
|
|||
assert result.principal == PRINCIPAL
|
||||
|
||||
|
||||
def test_legacy_session_token_is_invalid_without_grace():
|
||||
def test_legacy_session_token_is_invalid_when_grace_is_explicitly_disabled():
|
||||
minted: Final = mint_session_token(PRINCIPAL, _legacy_session_keys(MASTER_KEY), NOW)
|
||||
assert isinstance(minted, MintedSessionToken)
|
||||
assert legacy_session_keys_from_master_key(MASTER_KEY, KEYS, environ=_environ(MappingProxyType({}))) is None
|
||||
assert (
|
||||
legacy_session_keys_from_master_key(
|
||||
MASTER_KEY, KEYS, environ=_environ(MappingProxyType({LEGACY_KDF_GRACE_ENV_VAR: "false"}))
|
||||
)
|
||||
is None
|
||||
)
|
||||
result: Final = resolve_session_bearer(f"Bearer {minted.token.get_secret_value()}", KEYS, NOW)
|
||||
assert isinstance(result, SessionBearerInvalid)
|
||||
assert result.expired is False
|
||||
|
|
@ -302,6 +308,43 @@ def test_grace_is_disabled_under_fips():
|
|||
assert grace is None
|
||||
|
||||
|
||||
def test_grace_stays_off_under_fips_even_with_the_default_on():
|
||||
"""FIPS deployments get no scrypt fallback even though the grace window now defaults on."""
|
||||
grace: Final = legacy_session_keys_from_master_key(
|
||||
MASTER_KEY,
|
||||
KEYS,
|
||||
environ=_environ(MappingProxyType({FIPS_MODE_ENV_VAR: "true"})),
|
||||
)
|
||||
assert grace is None
|
||||
|
||||
|
||||
def test_legacy_grace_is_on_by_default_without_the_variable():
|
||||
legacy: Final = _legacy_session_keys(MASTER_KEY)
|
||||
grace: Final = legacy_session_keys_from_master_key(MASTER_KEY, KEYS, environ=_environ(MappingProxyType({})))
|
||||
assert grace == legacy
|
||||
|
||||
|
||||
def test_legacy_scrypt_derivation_is_cached_per_process(monkeypatch):
|
||||
"""Repeated legacy-format token attempts must not re-run the memory-hard scrypt KDF."""
|
||||
calls: Final[list[int]] = []
|
||||
real_scrypt = hashlib.scrypt
|
||||
|
||||
def counting_scrypt(password, **kwargs):
|
||||
calls.append(1)
|
||||
return real_scrypt(password, **kwargs)
|
||||
|
||||
monkeypatch.setattr(hashlib, "scrypt", counting_scrypt)
|
||||
legacy_scrypt.cache_clear()
|
||||
try:
|
||||
first: Final = legacy_scrypt(MASTER_KEY, b"litellm-mcp-gateway:session-signing:")
|
||||
second: Final = legacy_scrypt(MASTER_KEY, b"litellm-mcp-gateway:session-signing:")
|
||||
finally:
|
||||
legacy_scrypt.cache_clear()
|
||||
assert first is not None
|
||||
assert first == second
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_legacy_session_refresh_token_opens_during_grace():
|
||||
legacy: Final = _legacy_session_keys(MASTER_KEY)
|
||||
minted: Final = mint_session_refresh_token(PRINCIPAL, legacy, NOW)
|
||||
|
|
|
|||
|
|
@ -2632,10 +2632,43 @@ async def test_claude_code_refresh_of_a_legacy_scrypt_token_is_renewed_during_th
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_claude_code_refresh_of_a_legacy_scrypt_token_is_rejected_without_grace(monkeypatch):
|
||||
"""Without the grace variable the same scrypt-minted token answers invalid_grant and
|
||||
the minter never runs."""
|
||||
async def test_claude_code_refresh_of_a_legacy_scrypt_token_is_renewed_by_default_without_the_grace_variable(
|
||||
monkeypatch,
|
||||
):
|
||||
"""The grace window defaults on: with no ``LITELLM_MCP_LEGACY_KDF_GRACE`` in the
|
||||
environment a scrypt-minted refresh token still renews, so operators do not lose
|
||||
main-era tokens on upgrade day because they never heard of the variable."""
|
||||
monkeypatch.delenv("LITELLM_MCP_LEGACY_KDF_GRACE", raising=False)
|
||||
monkeypatch.delenv("LITELLM_FIPS_MODE", raising=False)
|
||||
minter: Final = _Minter()
|
||||
renewed: Final = await refresh_proxy_credential(
|
||||
refresh_token=_legacy_session_refresh(),
|
||||
client_id="claude_code",
|
||||
master_key=MASTER_KEY,
|
||||
cache=DualCache(),
|
||||
mint_proxy_credential=minter,
|
||||
)
|
||||
assert renewed.status_code == 200, renewed.body
|
||||
|
||||
|
||||
def _hkdf_session_refresh(user_id="u1", client_id="claude_code", team_id="team-b"):
|
||||
minted: Final = mint_session_refresh_token(
|
||||
SessionPrincipal(user_id=user_id, client_id=client_id, audience="proxy_api", team_id=team_id),
|
||||
session_keys_from_master_key(MASTER_KEY),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
assert isinstance(minted, MintedSessionToken)
|
||||
return minted.token.get_secret_value()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_claude_code_refresh_rejects_a_legacy_token_but_renews_an_hkdf_one_when_grace_is_explicitly_disabled(
|
||||
monkeypatch,
|
||||
):
|
||||
"""``LITELLM_MCP_LEGACY_KDF_GRACE=false`` is the operator's opt-out: the scrypt token
|
||||
answers invalid_grant without running the minter, while an HKDF control renews fine."""
|
||||
monkeypatch.setenv("LITELLM_MCP_LEGACY_KDF_GRACE", "false")
|
||||
monkeypatch.delenv("LITELLM_FIPS_MODE", raising=False)
|
||||
minter: Final = _Minter()
|
||||
refused: Final = await refresh_proxy_credential(
|
||||
refresh_token=_legacy_session_refresh(),
|
||||
|
|
@ -2647,3 +2680,12 @@ async def test_claude_code_refresh_of_a_legacy_scrypt_token_is_rejected_without_
|
|||
assert refused.status_code == 400
|
||||
assert json.loads(refused.body)["error"] == "invalid_grant"
|
||||
assert minter.calls == []
|
||||
control: Final = await refresh_proxy_credential(
|
||||
refresh_token=_hkdf_session_refresh(),
|
||||
client_id="claude_code",
|
||||
master_key=MASTER_KEY,
|
||||
cache=DualCache(),
|
||||
mint_proxy_credential=minter,
|
||||
)
|
||||
assert control.status_code == 200, control.body
|
||||
assert json.loads(control.body)["access_token"] == "sk-cli-u1"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue