Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/anthropic/lib/credentials/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from ._cache import TokenCache
from ._constants import OAUTH_API_BETA_HEADER
from ..._exceptions import CredentialsError

__all__ = ["AccessTokenAuth"]

Expand Down Expand Up @@ -84,7 +85,12 @@ def __init__(self, token_cache: TokenCache) -> None:
def _has_static_credential(request: httpx2.Request) -> bool:
return bool(request.headers.get("X-Api-Key") or request.headers.get("Authorization"))

def _apply(self, request: httpx2.Request, token: str) -> None:
def _apply(self, request: httpx2.Request, token: object) -> None:
if not isinstance(token, str) or not token or token != token.strip():
raise CredentialsError(
"Credentials provider returned an invalid access token; expected a non-empty string "
"without surrounding whitespace."
)
request.headers["Authorization"] = f"Bearer {token}"
existing_beta = request.headers.get("anthropic-beta", "")
# Tokenize the comma-separated header so dedupe matches whole flag
Expand Down
81 changes: 81 additions & 0 deletions tests/lib/test_access_token_auth_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from __future__ import annotations

from typing import Any, cast

import httpx2
import pytest

from anthropic import CredentialsError
from anthropic.lib.credentials._auth import AccessTokenAuth
from anthropic.lib.credentials._cache import TokenCache


class _TokenCacheStub:
def __init__(self, token: Any) -> None:
self.token = token
self.calls = 0

def get_token(self) -> str:
self.calls += 1
return cast(str, self.token)

async def async_get_token(self) -> str:
self.calls += 1
return cast(str, self.token)


def _auth(token: Any) -> tuple[AccessTokenAuth, _TokenCacheStub]:
cache = _TokenCacheStub(token)
return AccessTokenAuth(cast(TokenCache, cache)), cache


@pytest.mark.parametrize("token", ["", " ", " token", "token ", "\ttoken", None])
def test_sync_auth_rejects_invalid_provider_tokens(token: Any) -> None:
auth, cache = _auth(token)
request = httpx2.Request("GET", "https://api.anthropic.com/v1/models")

with pytest.raises(CredentialsError, match="invalid access token"):
list(auth.sync_auth_flow(request))

assert cache.calls == 1
assert "Authorization" not in request.headers


@pytest.mark.asyncio
@pytest.mark.parametrize("token", ["", " ", " token", "token ", "\ttoken", None])
async def test_async_auth_rejects_invalid_provider_tokens(token: Any) -> None:
auth, cache = _auth(token)
request = httpx2.Request("GET", "https://api.anthropic.com/v1/models")

with pytest.raises(CredentialsError, match="invalid access token"):
[item async for item in auth.async_auth_flow(request)]

assert cache.calls == 1
assert "Authorization" not in request.headers


def test_valid_provider_token_is_applied() -> None:
auth, cache = _auth("access-token")
request = httpx2.Request("GET", "https://api.anthropic.com/v1/models")

yielded = list(auth.sync_auth_flow(request))

assert yielded == [request]
assert cache.calls == 1
assert request.headers["Authorization"] == "Bearer access-token"
assert "oauth-2025-04-20" in request.headers["anthropic-beta"]


def test_static_authorization_still_bypasses_provider_validation() -> None:
auth, cache = _auth("")
request = httpx2.Request(
"GET",
"https://api.anthropic.com/v1/models",
headers={"Authorization": "Bearer explicit-token"},
)

yielded = list(auth.sync_auth_flow(request))

assert yielded == [request]
assert cache.calls == 0
assert request.headers["Authorization"] == "Bearer explicit-token"
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.