refactor(proxy/auth): normalize Bearer prefix in safe-hash helper (#29343)

* refactor(proxy/auth): normalize Bearer prefix in safe-hash helper

UserAPIKeyAuth._safe_hash_litellm_api_key now strips a leading
"Bearer "/"bearer " prefix before its existing sk-/JWT classification, so
the helper produces the same hashed output regardless of whether the
caller stripped the Authorization header prefix or passed the header
value through unchanged.

* refactor(proxy/auth): make Bearer-prefix strip case-insensitive

Per RFC 7235 the HTTP authorization scheme token is case-insensitive.
Replace the two-prefix loop with a single case-insensitive check so the
helper normalizes "Bearer ", "bearer ", "BEARER ", and any mixed-case
variant before classifying the remainder as sk- or JWT. The contract
test gains coverage of "BEARER " and "BeArEr ".

* test(mcp): align auth-handler test expectations with safe-hash helper

The two MCP auth tests asserted that UserAPIKeyAuth(api_key="Bearer ...")
retained the raw header bytes on the api_key field. _safe_hash_litellm_api_key
now normalizes that input — stripping the Bearer prefix and hashing the
resulting sk- key — so the expectations move to the normalized form:
the bare token in the parametrize case, and hash_token("sk-...") in the
backward-compat assertion. This matches what the real auth flow produces
(the builder strips Bearer and the DB stores the hashed token), so the
mocks now line up with production rather than with the un-normalized
validator output.
This commit is contained in:
yuneng-jiang
2026-05-30 14:04:22 -07:00
committed by GitHub
parent bfbb5d2375
commit 94a043efb2
3 changed files with 30 additions and 7 deletions
+8 -5
View File
@@ -2747,13 +2747,16 @@ class UserAPIKeyAuth(
1. Regular API keys from LiteLLM DB
2. JWT tokens used for connecting to LiteLLM API
"""
if api_key.startswith("sk-"):
return hash_token(api_key)
normalized = api_key
if normalized[:7].lower() == "bearer ":
normalized = normalized[7:]
if normalized.startswith("sk-"):
return hash_token(normalized)
from litellm.proxy.auth.handle_jwt import JWTHandler
if JWTHandler.is_jwt(token=api_key):
return f"hashed-jwt-{hash_token(token=api_key)}"
return api_key
if JWTHandler.is_jwt(token=normalized):
return f"hashed-jwt-{hash_token(token=normalized)}"
return normalized
@classmethod
def get_litellm_internal_health_check_user_api_key_auth(cls) -> "UserAPIKeyAuth":
@@ -213,7 +213,7 @@ class TestMCPRequestHandler:
# Test case 2: Authorization header present (fallback)
(
[(b"authorization", b"Bearer test-auth-token")],
"Bearer test-auth-token",
"test-auth-token",
None,
{},
),
@@ -674,7 +674,9 @@ class TestMCPOAuth2AuthFlow:
) = await MCPRequestHandler.process_mcp_request(scope)
# Should succeed with the LiteLLM key from Authorization header
assert auth_result.api_key == "Bearer sk-litellm-valid-key"
from litellm.proxy.utils import hash_token
assert auth_result.api_key == hash_token("sk-litellm-valid-key")
mock_auth.assert_called_once()
async def test_non_auth_http_exception_still_raises(self):
@@ -69,3 +69,21 @@ def test_internal_jobs_user_has_proxy_admin_role():
assert system_user.user_id == "system"
assert system_user.team_id == "system"
assert system_user.team_alias == "system"
def test_user_api_key_auth_hashes_authorization_header_form_of_key():
from litellm.proxy._types import UserAPIKeyAuth
raw_key = "sk-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
baseline = UserAPIKeyAuth(api_key=raw_key)
for header_form in (
f"Bearer {raw_key}",
f"bearer {raw_key}",
f"BEARER {raw_key}",
f"BeArEr {raw_key}",
):
from_header = UserAPIKeyAuth(api_key=header_form)
assert from_header.api_key == baseline.api_key
assert from_header.token == baseline.token
assert not from_header.api_key.lower().startswith("bearer")