Skip to content

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

Merged
yuneng-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_/adoring-stonebraker-c14597
May 30, 2026
Merged

refactor(proxy/auth): normalize Bearer prefix in safe-hash helper#29343
yuneng-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_/adoring-stonebraker-c14597

Conversation

@yuneng-berri

@yuneng-berri yuneng-berri commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Relevant issues

Linear ticket

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 unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

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

CI (LiteLLM team)

  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Run a proxy with prometheus enabled and a key whose owning user has an exceeded budget; the failure-metric label is now hashed regardless of whether the caller stripped the Bearer prefix:

python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.log

# Mint a user whose budget is already exceeded, and a key tied to that user.
USER_ID=$(curl -s http://localhost:4000/user/new \
  -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
  -d '{"max_budget": 0.0000001, "user_role": "internal_user"}' \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["user_id"])')
curl -s http://localhost:4000/user/update \
  -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
  -d "{\"user_id\": \"$USER_ID\", \"spend\": 1.0}" >/dev/null
KEY=$(curl -s http://localhost:4000/key/generate \
  -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
  -d "{\"user_id\": \"$USER_ID\", \"duration\": \"1h\"}" \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["key"])')

# Hit /chat/completions and /v1/messages with that key; both return 429 BudgetExceededError.
for path in chat/completions v1/messages; do
  curl -s http://localhost:4000/$path \
    -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
    -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}],"max_tokens":10}' \
    -o /dev/null -w "$path http=%{http_code}\n"
done

# Inspect the metric: hashed_api_key is the 64-char sha256, never a Bearer-prefixed string.
curl -sL http://localhost:4000/metrics -H 'Authorization: Bearer sk-1234' \
  | grep '^litellm_proxy_failed_requests_metric_total{'

Before this PR, the metric rows for both routes carried hashed_api_key="Bearer sk-…" literally; after, the same rows carry the proper sha256 hash.

The new contract test covers the same property at the helper level:

uv run pytest tests/test_litellm/proxy/test_proxy_types.py::test_user_api_key_auth_hashes_authorization_header_form_of_key -v

Type

🧹 Refactoring
✅ Test

Changes

UserAPIKeyAuth._safe_hash_litellm_api_key now strips a leading Bearer / bearer prefix before its existing sk- / JWT classification, so callers that pass the raw Authorization header value get the same hashed output as callers that strip the prefix themselves. Adds a contract test (test_user_api_key_auth_hashes_authorization_header_form_of_key) that asserts the helper produces the same hashed api_key and token fields for Bearer <sk>, bearer <sk>, and bare <sk> inputs.


Note

Low Risk
Small, localized change to key normalization for logging/metrics with a focused unit test; no auth policy or storage changes.

Overview
UserAPIKeyAuth._safe_hash_litellm_api_key now strips a leading Bearer or bearer before applying the existing sk- / JWT hashing logic, so callers that pass the raw Authorization header value get the same hashed api_key and token as callers that pass the bare key.

That fixes cases where observability labels (e.g. Prometheus hashed_api_key on failed requests) previously showed the literal Bearer sk-… string instead of the SHA-256 hash.

A contract test asserts Bearer, bearer, and bare sk- inputs all normalize to the same hashed fields.

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

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.
@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a metrics/observability bug where UserAPIKeyAuth._safe_hash_litellm_api_key would embed the raw Bearer <key> string in Prometheus labels instead of the SHA-256 hash when callers passed the full Authorization header value.

  • _safe_hash_litellm_api_key now performs a case-insensitive strip of a leading Bearer prefix (via [:7].lower()) before the existing sk-/JWT classification, so all call-sites produce the same hashed output regardless of whether the prefix is present.
  • Two MCP auth test assertions are updated to reflect the new behavior: the non-sk token test drops the embedded Bearer string, and the sk- OAuth2 flow test now asserts the properly hashed value instead of the old raw "Bearer sk-…" string (net improvement in assertion strength).
  • A new contract test (test_user_api_key_auth_hashes_authorization_header_form_of_key) verifies that Bearer, bearer, BEARER, and BeArEr variants all normalize to the same api_key and token as the bare key.

Confidence Score: 5/5

Safe to merge — change is limited to a logging/metrics normalization helper with no effect on auth policy or token storage.

The modification is entirely within _safe_hash_litellm_api_key, a helper used only for hashing keys before they appear in logs and metrics. It does not touch the auth decision path, token validation, or database layer. The case-insensitive prefix strip ([:7].lower()) is correct for all Bearer casing variants. Updated MCP tests reflect the new behavior without weakening assertions — the OAuth2 flow test actually tightens its assertion from a raw string to a proper hash.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_types.py Adds case-insensitive Bearer prefix stripping in _safe_hash_litellm_api_key before sk-/JWT classification; fix is correct and tightly scoped.
tests/test_litellm/proxy/test_proxy_types.py Adds a contract test covering Bearer, bearer, BEARER, and BeArEr prefix variants — all normalizing to the same hashed output as the bare key.
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py Two assertions updated to match new behavior: test case 2 drops the raw Bearer string expectation; OAuth2 flow test now asserts the properly hashed sk- key rather than the old raw Bearer string (tightens coverage).

Reviews (2): Last reviewed commit: "test(mcp): align auth-handler test expec..." | Re-trigger Greptile

Comment thread litellm/proxy/_types.py Outdated
@codecov

codecov Bot commented May 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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 ".
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.
@yuneng-berri

Copy link
Copy Markdown
Collaborator Author

@greptile

@ryan-crabbe-berri ryan-crabbe-berri 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.

lgtm

@yuneng-berri
yuneng-berri merged commit 94a043e into litellm_internal_staging May 30, 2026
116 of 118 checks passed
yuneng-berri added a commit that referenced this pull request May 31, 2026
* [internal copy of #29089] fix: duplicate claude code traces (#29311)

* 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.

---------

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
yuneng-berri added a commit that referenced this pull request May 31, 2026
…9343) (#29362)

* 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.
yuneng-berri added a commit that referenced this pull request May 31, 2026
…9343) (#29365)

* 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.
mateo-berri added a commit that referenced this pull request Jun 2, 2026
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…rriAI#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.
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