From b421a9414c25364063f4865e11a33ea56e197131 Mon Sep 17 00:00:00 2001 From: Alexander Loechel Date: Sat, 27 Sep 2025 20:25:13 +0200 Subject: [PATCH 1/6] Add RFC 6750 Bearer Token Auth --- httpx/__init__.py | 1 + httpx/_auth.py | 19 ++++++++++++++++++- tests/test_auth.py | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/httpx/__init__.py b/httpx/__init__.py index e9addde071..a727e499ed 100644 --- a/httpx/__init__.py +++ b/httpx/__init__.py @@ -38,6 +38,7 @@ def main() -> None: # type: ignore "Auth", "BaseTransport", "BasicAuth", + "BearerTokenAuth", "ByteStream", "Client", "CloseError", diff --git a/httpx/_auth.py b/httpx/_auth.py index b03971ab4b..2943787c2f 100644 --- a/httpx/_auth.py +++ b/httpx/_auth.py @@ -16,7 +16,7 @@ from hashlib import _Hash -__all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"] +__all__ = ["Auth", "BasicAuth", "BearerTokenAuth", "DigestAuth", "NetRCAuth"] class Auth: @@ -142,6 +142,23 @@ def _build_auth_header(self, username: str | bytes, password: str | bytes) -> st return f"Basic {token}" +class BearerTokenAuth(Auth): + """ + Allows the 'auth' argument to be passed as a bearer token string, + and uses HTTP Bearer authentication (RFC 6750). + """ + + def __init__(self, bearer_token: str | bytes) -> None: + self._auth_header = self._build_auth_header(bearer_token) + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + request.headers["Authorization"] = self._auth_header + yield request + + def _build_auth_header(self, bearer_token: str | bytes) -> str: + return f"Bearer {to_bytes(bearer_token).decode()}" + + class NetRCAuth(Auth): """ Use a 'netrc' file to lookup basic auth credentials based on the url host. diff --git a/tests/test_auth.py b/tests/test_auth.py index 6b6df922ea..25e9a2cba7 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -26,6 +26,21 @@ def test_basic_auth(): flow.send(response) +def test_bearer_token_auth(): + auth = httpx.BearerTokenAuth(bearer_token="my_token") + request = httpx.Request("GET", "https://www.example.com") + + # The initial request should include a bearer token auth header. + flow = auth.sync_auth_flow(request) + request = next(flow) + assert request.headers["Authorization"].startswith("Bearer ") + + # No other requests are made. + response = httpx.Response(content=b"Hello, world!", status_code=200) + with pytest.raises(StopIteration): + flow.send(response) + + def test_digest_auth_with_200(): auth = httpx.DigestAuth(username="user", password="pass") request = httpx.Request("GET", "https://www.example.com") From dc4a6e5c4618f68466c088e4ad4f98836423142b Mon Sep 17 00:00:00 2001 From: Alexander Loechel Date: Sat, 27 Sep 2025 21:24:30 +0200 Subject: [PATCH 2/6] add documentation for BearerTokenAuth --- docs/advanced/authentication.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md index 63d26e5f46..4b75a6fefc 100644 --- a/docs/advanced/authentication.md +++ b/docs/advanced/authentication.md @@ -16,7 +16,7 @@ Or configured on the client instance, ensuring that all outgoing requests will i ## Basic authentication -HTTP basic authentication is an unencrypted authentication scheme that uses a simple encoding of the username and password in the request `Authorization` header. Since it is unencrypted it should typically only be used over `https`, although this is not strictly enforced. +HTTP basic authentication ([RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617)) is an unencrypted authentication scheme that uses a simple encoding of the username and password in the request `Authorization` header. Since it is unencrypted it should typically only be used over `https`, although this is not strictly enforced. ```pycon >>> auth = httpx.BasicAuth(username="finley", password="secret") @@ -26,9 +26,28 @@ HTTP basic authentication is an unencrypted authentication scheme that uses a si ``` +## Bearer Token authentication + +Bearer Token authentication ([RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)) is an unencrypted authentication scheme that uses an API key (Bearer Token) to access OAuth 2.0-protected resources. +There are three variants to transmit the Token: + +* `Authorization` Request Header Field +* Form-Encoded Body Parameter +* URI Query Parameter + +Since it is unencrypted it should typically only be used over `https`, although this is not strictly enforced. + +```pycon +>>> auth = httpx.BearerTokenAuth(bearer_token="secret") +>>> client = httpx.Client(auth=auth) +>>> response = client.get("https://httpbin.org/bearer") +>>> response + +``` + ## Digest authentication -HTTP digest authentication is a challenge-response authentication scheme. Unlike basic authentication it provides encryption, and can be used over unencrypted `http` connections. It requires an additional round-trip in order to negotiate the authentication. +HTTP digest authentication ([RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616)) is a challenge-response authentication scheme. Unlike basic authentication it provides encryption, and can be used over unencrypted `http` connections. It requires an additional round-trip in order to negotiate the authentication. ```pycon >>> auth = httpx.DigestAuth(username="olivia", password="secret") From 8cf94a7e00068c7f8a2f2aa6a480d001d7c8b5b5 Mon Sep 17 00:00:00 2001 From: Alexander Loechel Date: Sat, 27 Sep 2025 21:24:42 +0200 Subject: [PATCH 3/6] Add Changelog entry --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13bbfcdb79..4508e1831d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [UNRELEASED] +### Added + +* Support for `BearerTokenAuth`. + ### Removed * Drop support for Python 3.8 @@ -13,7 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## 0.28.1 (6th December, 2024) * Fix SSL case where `verify=False` together with client side certificates. - + ## 0.28.0 (28th November, 2024) Be aware that the default *JSON request bodies now use a more compact representation*. This is generally considered a prefered style, tho may require updates to test suites. From 9f978a33e4015cc4527513a27087c387db4b367d Mon Sep 17 00:00:00 2001 From: Alexander Loechel Date: Sat, 27 Sep 2025 21:49:42 +0200 Subject: [PATCH 4/6] RFC variant selector added --- docs/advanced/authentication.md | 4 ++-- httpx/_auth.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md index 4b75a6fefc..8da8ea9061 100644 --- a/docs/advanced/authentication.md +++ b/docs/advanced/authentication.md @@ -32,8 +32,8 @@ Bearer Token authentication ([RFC 6750](https://datatracker.ietf.org/doc/html/rf There are three variants to transmit the Token: * `Authorization` Request Header Field -* Form-Encoded Body Parameter -* URI Query Parameter +* Form-Encoded Body Parameter (not implemented) +* URI Query Parameter (not implemented) Since it is unencrypted it should typically only be used over `https`, although this is not strictly enforced. diff --git a/httpx/_auth.py b/httpx/_auth.py index 2943787c2f..9e05433bd0 100644 --- a/httpx/_auth.py +++ b/httpx/_auth.py @@ -148,7 +148,9 @@ class BearerTokenAuth(Auth): and uses HTTP Bearer authentication (RFC 6750). """ - def __init__(self, bearer_token: str | bytes) -> None: + def __init__(self, bearer_token: str | bytes, variant: typing.Literal["HEADER", "FORM-ENCODED", "QUERY"] = "HEADER") -> None: + if variant != "HEADER": + raise NotImplementedError(f"BearerTokenAuth variant '{variant}' is not yet implemented") self._auth_header = self._build_auth_header(bearer_token) def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: From f9e10f17f968b5e7da13943f72af4f3ca54681b1 Mon Sep 17 00:00:00 2001 From: Alexander Loechel Date: Sat, 27 Sep 2025 21:52:43 +0200 Subject: [PATCH 5/6] lint and reformat --- httpx/_auth.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/httpx/_auth.py b/httpx/_auth.py index 9e05433bd0..376a9ed5d5 100644 --- a/httpx/_auth.py +++ b/httpx/_auth.py @@ -148,9 +148,15 @@ class BearerTokenAuth(Auth): and uses HTTP Bearer authentication (RFC 6750). """ - def __init__(self, bearer_token: str | bytes, variant: typing.Literal["HEADER", "FORM-ENCODED", "QUERY"] = "HEADER") -> None: + def __init__( + self, + bearer_token: str | bytes, + variant: typing.Literal["HEADER", "FORM-ENCODED", "QUERY"] = "HEADER", + ) -> None: if variant != "HEADER": - raise NotImplementedError(f"BearerTokenAuth variant '{variant}' is not yet implemented") + raise NotImplementedError( + f"BearerTokenAuth variant '{variant}' is not yet implemented" + ) self._auth_header = self._build_auth_header(bearer_token) def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: From a28cca91cbc0a8677f3a2c0a50c8a46059792941 Mon Sep 17 00:00:00 2001 From: Alexander Loechel Date: Sat, 27 Sep 2025 21:58:45 +0200 Subject: [PATCH 6/6] add test for bearer token auth varaiants --- tests/test_auth.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 25e9a2cba7..84a4860041 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -26,8 +26,8 @@ def test_basic_auth(): flow.send(response) -def test_bearer_token_auth(): - auth = httpx.BearerTokenAuth(bearer_token="my_token") +def test_bearer_token_auth_header(): + auth = httpx.BearerTokenAuth(bearer_token="secret") request = httpx.Request("GET", "https://www.example.com") # The initial request should include a bearer token auth header. @@ -41,6 +41,22 @@ def test_bearer_token_auth(): flow.send(response) +def test_bearer_token_auth_form_encoded(): + with pytest.raises( + NotImplementedError, + ): + auth = httpx.BearerTokenAuth(bearer_token="secret", variant="FORM-ENCODED") + assert auth # pragma: no cover + + +def test_bearer_token_auth_query(): + with pytest.raises( + NotImplementedError, + ): + auth = httpx.BearerTokenAuth(bearer_token="secret", variant="QUERY") + assert auth # pragma: no cover + + def test_digest_auth_with_200(): auth = httpx.DigestAuth(username="user", password="pass") request = httpx.Request("GET", "https://www.example.com")