From 3c1991887f9874ff3259cd2079a399b92cf28d6c Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Sun, 23 Aug 2026 13:06:02 +0530 Subject: [PATCH] fix(auth): match the Bearer auth scheme case-insensitively (RFC 7235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `classify_auth_signals` gated PAYG/OAUTH token-shape detection on `authorization.startswith("Bearer ")`. RFC 7235 §2.1 defines the auth-scheme token as case-insensitive, so a client sending `Authorization: bearer sk-...` skipped the Bearer branch entirely and fell through to the `elif auth:` OAUTH classification — misclassifying a PAYG API key as OAUTH. Auth mode drives compression-policy routing and the cost/TOIN `auth_mode` label, so the misclassification silently changes how a request is compressed and accounted. Fold only the scheme for the comparison via `str.partition`; the credential keeps its original case (tokens are case-sensitive). --- headroom/proxy/auth_policy.py | 11 +++++++++-- tests/test_auth_policy.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/headroom/proxy/auth_policy.py b/headroom/proxy/auth_policy.py index 73f6cf91d..7e55cc95a 100644 --- a/headroom/proxy/auth_policy.py +++ b/headroom/proxy/auth_policy.py @@ -71,8 +71,15 @@ def classify_auth_signals(signals: AuthSignals) -> AuthMode: return AuthMode.SUBSCRIPTION auth = signals.authorization - if auth.startswith("Bearer "): - token = auth[len("Bearer ") :] + # The auth-scheme token ("Bearer") is case-insensitive per RFC 7235 §2.1, + # so a client sending `Authorization: bearer sk-...` must classify the same + # as `Bearer`. A case-sensitive `startswith("Bearer ")` sent such a request + # to the `elif auth:` (OAUTH) fallthrough, misclassifying a PAYG API key — + # which mis-routes compression policy and mislabels cost/TOIN auth_mode. + # Only the scheme is case-folded; the credential itself stays case-sensitive. + scheme, sep, credentials = auth.partition(" ") + if sep and scheme.lower() == "bearer": + token = credentials if token.startswith("sk-ant-oat"): return AuthMode.OAUTH if token.startswith("sk-ant-api") or token.startswith("sk-"): diff --git a/tests/test_auth_policy.py b/tests/test_auth_policy.py index e15c3552b..25417fabe 100644 --- a/tests/test_auth_policy.py +++ b/tests/test_auth_policy.py @@ -60,3 +60,34 @@ def test_codex_stamp_only_for_unidentified_responses_callers() -> None: is False ) assert should_stamp_codex_client_signals("/v1/chat/completions", AuthSignals()) is False + + +def test_bearer_scheme_is_case_insensitive() -> None: + # RFC 7235 §2.1: the auth-scheme token is case-insensitive. A lowercase or + # mixed-case "bearer" carrying a PAYG key must classify as PAYG, not fall + # through to the non-Bearer OAUTH branch. + for scheme in ("bearer", "BEARER", "Bearer", "BeArEr"): + signals = AuthSignals(authorization=f"{scheme} sk-ant-api03-abc") + assert classify_auth_signals(signals) is AuthMode.PAYG, scheme + + +def test_bearer_case_insensitive_preserves_oauth_and_jwt_shapes() -> None: + jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.signature" + assert classify_auth_signals(AuthSignals(authorization="bearer sk-ant-oat01-abc")) is ( + AuthMode.OAUTH + ) + assert classify_auth_signals(AuthSignals(authorization=f"bearer {jwt}")) is AuthMode.OAUTH + + +def test_credential_case_is_not_folded() -> None: + # Only the scheme is case-folded; the token keeps its case, so a PAYG + # `sk-...` prefix still matches exactly (it is lowercase by construction). + signals = AuthSignals(authorization="Bearer sk-PROJ-Abc123") + assert classify_auth_signals(signals) is AuthMode.PAYG + + +def test_non_bearer_scheme_still_oauth() -> None: + # AWS SigV4 (Bedrock) and any other non-Bearer scheme keep the OAUTH + # passthrough-prefer classification. + signals = AuthSignals(authorization="AWS4-HMAC-SHA256 Credential=AKIA/...") + assert classify_auth_signals(signals) is AuthMode.OAUTH