From 94a043efb226c5ccdbfc028fbb930ce45fb965eb Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 30 May 2026 14:04:22 -0700 Subject: [PATCH] refactor(proxy/auth): normalize Bearer prefix in safe-hash helper (#29343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- litellm/proxy/_types.py | 13 ++++++++----- .../auth/test_user_api_key_auth_mcp.py | 6 ++++-- tests/test_litellm/proxy/test_proxy_types.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 522e85632d..95294f4838 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -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": diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index a7d9ce64f8..b3508e1312 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -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): diff --git a/tests/test_litellm/proxy/test_proxy_types.py b/tests/test_litellm/proxy/test_proxy_types.py index 0fa8679899..dbb952968e 100644 --- a/tests/test_litellm/proxy/test_proxy_types.py +++ b/tests/test_litellm/proxy/test_proxy_types.py @@ -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")