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. diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md index 63d26e5f46..8da8ea9061 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 (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. + +```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") 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..376a9ed5d5 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,31 @@ 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, + 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]: + 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..84a4860041 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -26,6 +26,37 @@ def test_basic_auth(): flow.send(response) +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. + 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_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")