fix(proxy): preserve and forward OAuth Authorization headers through proxy layer

PR #21039 fixed OAuth token handling at the LLM layer (Authorization: Bearer
instead of x-api-key), but the proxy layer still strips the Authorization
header in clean_headers() before it reaches the Anthropic code. This breaks
OAuth for proxy users (e.g., Claude Code Max through LiteLLM proxy).

Changes:
- Add is_anthropic_oauth_key() helper to detect OAuth tokens (sk-ant-oat*)
- Preserve OAuth Authorization headers in clean_headers() instead of stripping
- Forward OAuth Authorization via ProviderSpecificHeader in
  add_provider_specific_headers_to_request() so tokens only reach
  Anthropic-compatible providers (anthropic, bedrock, vertex_ai)

Fixes #19618
This commit is contained in:
Adam Reed 2026-02-13 23:37:34 -06:00 committed by shivam
parent 61ed8f9e03
commit 76f5cb85b8
3 changed files with 166 additions and 2 deletions

View file

@ -22,6 +22,15 @@ from litellm.types.llms.anthropic import (
from litellm.types.llms.openai import AllMessageValues
def is_anthropic_oauth_key(value: Optional[str]) -> bool:
"""Check if a value contains an Anthropic OAuth token (sk-ant-oat*)."""
if value is None:
return False
# Handle both raw token and "Bearer <token>" format
if value.startswith("Bearer "):
value = value[7:]
return value.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX)
def optionally_handle_anthropic_oauth(
headers: dict, api_key: Optional[str]
) -> tuple[dict, Optional[str]]:

View file

@ -237,6 +237,8 @@ def clean_headers(
"""
Removes litellm api key from headers
"""
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
clean_headers = {}
litellm_key_lower = (
litellm_key_header_name.lower() if litellm_key_header_name is not None else None
@ -244,8 +246,13 @@ def clean_headers(
for header, value in headers.items():
header_lower = header.lower()
# Preserve Authorization header if it contains Anthropic OAuth token (sk-ant-oat*)
# This allows OAuth tokens to be forwarded to Anthropic-compatible providers
# via add_provider_specific_headers_to_request()
if header_lower == "authorization" and is_anthropic_oauth_key(value):
clean_headers[header] = value
# Check if header should be excluded: either in special headers cache or matches custom litellm key
if header_lower not in _SPECIAL_HEADERS_CACHE and (
elif header_lower not in _SPECIAL_HEADERS_CACHE and (
litellm_key_lower is None or header_lower != litellm_key_lower
):
clean_headers[header] = value
@ -1605,6 +1612,8 @@ def add_provider_specific_headers_to_request(
data: dict,
headers: dict,
):
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
anthropic_headers = {}
# boolean to indicate if a header was added
added_header = False
@ -1614,6 +1623,14 @@ def add_provider_specific_headers_to_request(
anthropic_headers[header] = header_value
added_header = True
# Check for Authorization header with Anthropic OAuth token (sk-ant-oat*)
# This needs to be handled via provider-specific headers to ensure it only
# goes to Anthropic-compatible providers, not all providers in the router
for header, value in headers.items():
if header.lower() == "authorization" and is_anthropic_oauth_key(value):
anthropic_headers[header] = value
added_header = True
break
if added_header is True:
# Anthropic headers work across multiple providers
# Store as comma-separated list so retrieval can match any of them

View file

@ -81,4 +81,142 @@ def test_regular_api_keys_still_work():
# Regular key should be unchanged
assert extracted_api_key == regular_key
# OAuth headers should NOT be added
assert "anthropic-dangerous-direct-browser-access" not in updated_headers
assert "anthropic-dangerous-direct-browser-access" not in updated_headers
class TestIsAnthropicOAuthKey:
"""Tests for is_anthropic_oauth_key helper function."""
def test_oauth_token_raw(self):
"""Raw OAuth token should be detected."""
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
assert is_anthropic_oauth_key("sk-ant-oat01-abc123") is True
assert is_anthropic_oauth_key("sk-ant-oat02-xyz789") is True
def test_oauth_token_bearer_format(self):
"""Bearer-prefixed OAuth token should be detected."""
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
assert is_anthropic_oauth_key("Bearer sk-ant-oat01-abc123") is True
assert is_anthropic_oauth_key("Bearer sk-ant-oat02-xyz789") is True
def test_non_oauth_tokens(self):
"""Non-OAuth values should return False."""
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
assert is_anthropic_oauth_key(None) is False
assert is_anthropic_oauth_key("") is False
assert is_anthropic_oauth_key("sk-ant-api01-abc123") is False
assert is_anthropic_oauth_key("Bearer sk-ant-api01-abc123") is False
def test_case_sensitivity(self):
"""OAuth prefix matching should be case-sensitive."""
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
assert is_anthropic_oauth_key("sk-ant-OAT01-abc123") is False
assert is_anthropic_oauth_key("SK-ANT-OAT01-abc123") is False
def test_just_prefix(self):
"""Just the prefix with no suffix should still match."""
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
assert is_anthropic_oauth_key("sk-ant-oat") is True
class TestProxyOAuthHeaderForwarding:
"""Tests for proxy-layer OAuth header preservation and forwarding."""
def test_clean_headers_preserves_oauth_authorization(self):
"""clean_headers should preserve Authorization header with OAuth tokens."""
from starlette.datastructures import Headers
from litellm.proxy.litellm_pre_call_utils import clean_headers
raw_headers = Headers(
raw=[
(b"authorization", f"Bearer {FAKE_OAUTH_TOKEN}".encode()),
(b"content-type", b"application/json"),
]
)
cleaned = clean_headers(raw_headers)
assert "authorization" in cleaned
assert cleaned["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}"
assert cleaned["content-type"] == "application/json"
def test_clean_headers_strips_non_oauth_authorization(self):
"""clean_headers should strip Authorization header with regular API keys."""
from starlette.datastructures import Headers
from litellm.proxy.litellm_pre_call_utils import clean_headers
raw_headers = Headers(
raw=[
(b"authorization", b"Bearer sk-regular-key-123"),
(b"content-type", b"application/json"),
]
)
cleaned = clean_headers(raw_headers)
assert "authorization" not in cleaned
assert cleaned["content-type"] == "application/json"
def test_add_provider_specific_headers_forwards_oauth(self):
"""add_provider_specific_headers_to_request should forward OAuth Authorization
as a ProviderSpecificHeader scoped to Anthropic-compatible providers."""
from litellm.proxy.litellm_pre_call_utils import (
add_provider_specific_headers_to_request,
)
data: dict = {}
headers = {
"authorization": f"Bearer {FAKE_OAUTH_TOKEN}",
"content-type": "application/json",
}
add_provider_specific_headers_to_request(data=data, headers=headers)
assert "provider_specific_header" in data
psh = data["provider_specific_header"]
assert "anthropic" in psh["custom_llm_provider"]
assert "bedrock" in psh["custom_llm_provider"]
assert "vertex_ai" in psh["custom_llm_provider"]
assert psh["extra_headers"]["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}"
def test_add_provider_specific_headers_ignores_non_oauth(self):
"""add_provider_specific_headers_to_request should not create a
ProviderSpecificHeader for non-OAuth Authorization headers."""
from litellm.proxy.litellm_pre_call_utils import (
add_provider_specific_headers_to_request,
)
data: dict = {}
headers = {
"authorization": "Bearer sk-regular-key-123",
"content-type": "application/json",
}
add_provider_specific_headers_to_request(data=data, headers=headers)
assert "provider_specific_header" not in data
def test_add_provider_specific_headers_combines_anthropic_and_oauth(self):
"""When both anthropic-beta and OAuth Authorization are present, both
should be included in the ProviderSpecificHeader."""
from litellm.proxy.litellm_pre_call_utils import (
add_provider_specific_headers_to_request,
)
data: dict = {}
headers = {
"authorization": f"Bearer {FAKE_OAUTH_TOKEN}",
"anthropic-beta": "oauth-2025-04-20",
"content-type": "application/json",
}
add_provider_specific_headers_to_request(data=data, headers=headers)
assert "provider_specific_header" in data
psh = data["provider_specific_header"]
assert psh["extra_headers"]["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}"
assert psh["extra_headers"]["anthropic-beta"] == "oauth-2025-04-20"