Skip to content

Commit f4c70b2

Browse files
authored
Merge pull request #107 from Colin-b/develop
Release 0.23.0
2 parents 3fd7c07 + 0d9a3ac commit f4c70b2

34 files changed

+1040
-96
lines changed

Diff for: .github/workflows/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- name: Set up Python
1616
uses: actions/setup-python@v5
1717
with:
18-
python-version: '3.12'
18+
python-version: '3.13'
1919
- name: Create packages
2020
run: |
2121
python -m pip install build

Diff for: .github/workflows/test.yml

+11-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ubuntu-latest
99
strategy:
1010
matrix:
11-
python-version: ['3.9', '3.10', '3.11', '3.12']
11+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
1212

1313
steps:
1414
- uses: actions/checkout@v4
@@ -26,4 +26,13 @@ jobs:
2626
- name: Create packages
2727
run: |
2828
python -m pip install build
29-
python -m build .
29+
python -m build .
30+
rm -Rf httpx_auth
31+
- name: Install wheel
32+
run: |
33+
python -m pip install dist/httpx_auth-0.23.0-py3-none-any.whl --force-reinstall
34+
python -c 'import httpx_auth'
35+
- name: Install source distribution
36+
run: |
37+
python -m pip install dist/httpx_auth-0.23.0.tar.gz --force-reinstall
38+
python -c 'import httpx_auth'

Diff for: .pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
repos:
22
- repo: https://github.com/psf/black
3-
rev: 24.1.1
3+
rev: 24.8.0
44
hooks:
55
- id: black

Diff for: CHANGELOG.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.23.0] - 2025-01-07
10+
### Fixed
11+
- Bearer tokens with nested JSON string are now properly handled. Thanks to [`Patrick Rodrigues`](https://github.com/pythrick).
12+
- Client credentials auth instances will now use credentials (client_id and client_secret) as well to distinguish tokens. This was an issue when the only parameters changing were the credentials.
13+
14+
### Changed
15+
- Requires [`httpx`](https://www.python-httpx.org)==0.28.\*
16+
- Exceptions issued by `httpx_auth` are now inheriting from `httpx_auth.HttpxAuthException`, itself inheriting from `httpx.HTTPError`, instead of `Exception`.
17+
18+
### Added
19+
- Explicit support for python `3.13`.
20+
921
## [0.22.0] - 2024-03-02
1022
### Changed
1123
- Requires [`httpx`](https://www.python-httpx.org)==0.27.\*
@@ -250,7 +262,8 @@ Note that a few changes were made:
250262
### Added
251263
- Placeholder for port of requests_auth to httpx
252264

253-
[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.22.0...HEAD
265+
[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.23.0...HEAD
266+
[0.23.0]: https://github.com/Colin-b/httpx_auth/compare/v0.22.0...v0.23.0
254267
[0.22.0]: https://github.com/Colin-b/httpx_auth/compare/v0.21.0...v0.22.0
255268
[0.21.0]: https://github.com/Colin-b/httpx_auth/compare/v0.20.0...v0.21.0
256269
[0.20.0]: https://github.com/Colin-b/httpx_auth/compare/v0.19.0...v0.20.0

Diff for: README.md

+17-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Build status" src="https://github.com/Colin-b/httpx_auth/workflows/Release/badge.svg"></a>
66
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
77
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
8-
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-775 passed-blue"></a>
8+
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-783 passed-blue"></a>
99
<a href="https://pypi.org/project/httpx-auth/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/httpx_auth"></a>
1010
</p>
1111

12+
> [!NOTE]
1213
> Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0).
1314
>
1415
> However, current state can be considered as stable.
@@ -376,7 +377,7 @@ Note:
376377
| `early_expiry` | Number of seconds before actual token expiry where token will be considered as expired. Used to ensure token will not expire between the time of retrieval and the time the request reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry. | Optional | 30.0 |
377378
| `client` | `httpx.Client` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |
378379

379-
Any other parameter will be put as body parameters in the token URL.
380+
Any other parameter will be put as body parameters in the token URL.
380381

381382

382383
### Client Credentials flow
@@ -711,6 +712,8 @@ OAuth2.token_cache = JsonTokenFileCache('path/to/my_token_cache.json')
711712

712713
### Managing the web browser
713714

715+
#### Authentication response pages
716+
714717
You can configure the browser display settings thanks to `httpx_auth.OAuth2.display` as in the following:
715718
```python
716719
from httpx_auth import OAuth2, DisplaySettings
@@ -727,6 +730,16 @@ The following parameters can be provided to `DisplaySettings`:
727730
| `failure_display_time` | In case received code or token is not valid, this is the maximum amount of milliseconds the failure page will be displayed in your browser. | 10_000 |
728731
| `failure_html` | In case received code or token is not valid, this is the failure page that will be displayed in your browser. `{information}` and `{display_time}` are expected in this content. | |
729732

733+
#### Text-mode web browser
734+
735+
This project uses [`webbrowser.open()`][4] to open a web browser to support authentication flows like OAuth's Authorization Code grant. When running graphically, `webbrowser.open()` does not block. But when run in text mode, `webbrowser.open()` blocks until the opened browser is closed, which leads to a deadlock when httpx-auth cannot serve the auth response pages to the webbrowser. To work around this, you can specify a `BROWSER` environment variable that contains a `%s` and ends with a `&`, and the `webbrowser` module will open the text-mode browser in a subprocess and allow httpx-auth to serve the auth response pages to the browser without deadlocking.
736+
737+
```bash
738+
BROWSER="/usr/bin/links %s &"
739+
```
740+
741+
For more information, please see the implementation of [`webbrowser.get()`][5].
742+
730743
## AWS Signature v4
731744

732745
Amazon Web Service Signature version 4 is implemented following [Amazon S3 documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html) and [request-aws4auth 1.2.3](https://github.com/sam-washington/requests-aws4auth) (with some changes, see below).
@@ -996,4 +1009,6 @@ def test_something(browser_mock: BrowserMock):
9961009
[1]: https://pypi.python.org/pypi/httpx "httpx module"
9971010
[2]: https://www.python-httpx.org/advanced/#customizing-authentication "authentication parameter on httpx module"
9981011
[3]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken "OpenID ID Token specifications"
1012+
[4]: https://docs.python.org/3/library/webbrowser.html#webbrowser.open "Python webbrowser module"
1013+
[5]: https://github.com/python/cpython/blob/main/Lib/webbrowser.py "Python webbrowser module code"
9991014
[6]: https://docs.pytest.org/en/latest/ "pytest module"

Diff for: httpx_auth/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
InvalidToken,
4141
TokenExpiryNotProvided,
4242
InvalidGrantRequest,
43+
HttpxAuthException,
4344
)
4445
from httpx_auth.version import __version__
4546

@@ -67,6 +68,7 @@
6768
"JsonTokenFileCache",
6869
"TokenMemoryCache",
6970
"AWS4Auth",
71+
"HttpxAuthException",
7072
"GrantNotProvided",
7173
"TimeoutOccurred",
7274
"AuthenticationFailed",

Diff for: httpx_auth/_errors.py

+19-14
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,42 @@
44
import httpx
55

66

7-
class AuthenticationFailed(Exception):
7+
class HttpxAuthException(httpx.HTTPError): ...
8+
9+
10+
class AuthenticationFailed(HttpxAuthException):
811
"""User was not authenticated."""
912

1013
def __init__(self):
11-
Exception.__init__(self, "User was not authenticated.")
14+
HttpxAuthException.__init__(self, "User was not authenticated.")
1215

1316

14-
class TimeoutOccurred(Exception):
17+
class TimeoutOccurred(HttpxAuthException):
1518
"""No response within timeout interval."""
1619

1720
def __init__(self, timeout: float):
18-
Exception.__init__(
21+
HttpxAuthException.__init__(
1922
self, f"User authentication was not received within {timeout} seconds."
2023
)
2124

2225

23-
class InvalidToken(Exception):
26+
class InvalidToken(HttpxAuthException):
2427
"""Token is invalid."""
2528

2629
def __init__(self, token_name: str):
27-
Exception.__init__(self, f"{token_name} is invalid.")
30+
HttpxAuthException.__init__(self, f"{token_name} is invalid.")
2831

2932

30-
class GrantNotProvided(Exception):
33+
class GrantNotProvided(HttpxAuthException):
3134
"""Grant was not provided."""
3235

3336
def __init__(self, grant_name: str, dictionary_without_grant: dict):
34-
Exception.__init__(
37+
HttpxAuthException.__init__(
3538
self, f"{grant_name} not provided within {dictionary_without_grant}."
3639
)
3740

3841

39-
class InvalidGrantRequest(Exception):
42+
class InvalidGrantRequest(HttpxAuthException):
4043
"""
4144
If the request failed client authentication or is invalid, the authorization server returns an error response as described in https://tools.ietf.org/html/rfc6749#section-5.2
4245
"""
@@ -64,7 +67,7 @@ class InvalidGrantRequest(Exception):
6467
}
6568

6669
def __init__(self, response: Union[httpx.Response, dict]):
67-
Exception.__init__(self, InvalidGrantRequest.to_message(response))
70+
HttpxAuthException.__init__(self, InvalidGrantRequest.to_message(response))
6871

6972
@staticmethod
7073
def to_message(response: Union[httpx.Response, dict]) -> str:
@@ -114,17 +117,19 @@ def _pop(key: str) -> str:
114117
return message
115118

116119

117-
class StateNotProvided(Exception):
120+
class StateNotProvided(HttpxAuthException):
118121
"""State was not provided."""
119122

120123
def __init__(self, dictionary_without_state: dict):
121-
Exception.__init__(
124+
HttpxAuthException.__init__(
122125
self, f"state not provided within {dictionary_without_state}."
123126
)
124127

125128

126-
class TokenExpiryNotProvided(Exception):
129+
class TokenExpiryNotProvided(HttpxAuthException):
127130
"""Token expiry was not provided."""
128131

129132
def __init__(self, token_body: dict):
130-
Exception.__init__(self, f"Expiry (exp) is not provided in {token_body}.")
133+
HttpxAuthException.__init__(
134+
self, f"Expiry (exp) is not provided in {token_body}."
135+
)

Diff for: httpx_auth/_oauth2/client_credentials.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
from hashlib import sha512
23
from typing import Union, Iterable
34

@@ -67,7 +68,10 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs)
6768
self.data["scope"] = " ".join(scope) if isinstance(scope, list) else scope
6869
self.data.update(kwargs)
6970

70-
all_parameters_in_url = _add_parameters(self.token_url, self.data)
71+
cache_data = copy.deepcopy(self.data)
72+
cache_data["_httpx_auth_client_id"] = self.client_id
73+
cache_data["_httpx_auth_client_secret"] = self.client_secret
74+
all_parameters_in_url = _add_parameters(self.token_url, cache_data)
7175
state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest()
7276

7377
super().__init__(

Diff for: httpx_auth/_oauth2/tokens.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def decode_base64(base64_encoded_string: str) -> str:
2828
missing_padding = len(base64_encoded_string) % 4
2929
if missing_padding != 0:
3030
base64_encoded_string += "=" * (4 - missing_padding)
31-
return base64.b64decode(base64_encoded_string).decode("unicode_escape")
31+
return base64.urlsafe_b64decode(base64_encoded_string).decode("utf-8")
3232

3333

3434
def is_expired(expiry: float, early_expiry: float) -> bool:

Diff for: httpx_auth/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
44
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
55
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
6-
__version__ = "0.22.0"
6+
__version__ = "0.23.0"

Diff for: pyproject.toml

+8-8
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ classifiers=[
2727
"Programming Language :: Python :: 3.10",
2828
"Programming Language :: Python :: 3.11",
2929
"Programming Language :: Python :: 3.12",
30+
"Programming Language :: Python :: 3.13",
3031
"Topic :: Software Development :: Build Tools",
3132
]
3233
dependencies = [
33-
"httpx==0.27.*",
34+
"httpx==0.28.*",
3435
]
3536
dynamic = ["version"]
3637

@@ -45,22 +46,21 @@ testing = [
4546
# Used to generate test tokens
4647
"pyjwt==2.*",
4748
# Used to mock httpx
48-
"pytest_httpx==0.30.*",
49+
"pytest_httpx==0.35.*",
4950
# Used to mock date and time
5051
"time-machine==2.*",
5152
# Used to check coverage
52-
"pytest-cov==4.*",
53+
"pytest-cov==6.*",
5354
# Used to run async tests
54-
"pytest-asyncio==0.23.*",
55+
"pytest-asyncio==0.25.*",
5556
]
5657

57-
[tool.setuptools.packages.find]
58-
exclude = ["tests*"]
59-
6058
[tool.setuptools.dynamic]
6159
version = {attr = "httpx_auth.version.__version__"}
6260

6361
[tool.pytest.ini_options]
6462
filterwarnings = [
6563
"error",
66-
]
64+
]
65+
# Silence deprecation warnings about option "asyncio_default_fixture_loop_scope"
66+
asyncio_default_fixture_loop_scope = "function"

Diff for: tests/aws_signature_v4/test_aws4auth_async.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ async def test_aws_auth_with_content_in_request(httpx_mock: HTTPXMock):
4747
method="POST",
4848
match_json=[{"key": "value"}],
4949
match_headers={
50-
"x-amz-content-sha256": "fb65c1441d6743274738fe3b3042a73167ba1fb2d34679d8dd16433473758f97",
51-
"Authorization": "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=5f4f832a19fc834d4f34047289ad67d96da25bd414a70f02ce6b85aef9ab8068",
50+
"x-amz-content-sha256": "1e1d3e3fb0bcfb7b2b61f687369d0227e6aefd6739e1182312382ab03e83b75f",
51+
"Authorization": "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=680fe73ca28e1639a3b2337a68d83324e03742679e612a52d3d29c9b6fc4b512",
5252
"x-amz-date": "20181011T150505Z",
5353
},
5454
)
@@ -470,8 +470,8 @@ async def test_aws_auth_with_security_token_and_content_in_request(
470470
method="POST",
471471
match_json=[{"key": "value"}],
472472
match_headers={
473-
"x-amz-content-sha256": "fb65c1441d6743274738fe3b3042a73167ba1fb2d34679d8dd16433473758f97",
474-
"Authorization": "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=e02c4733589cf6e80361f6905564da6d0c23a0829bb3c3899b328e43b2f7b581",
473+
"x-amz-content-sha256": "1e1d3e3fb0bcfb7b2b61f687369d0227e6aefd6739e1182312382ab03e83b75f",
474+
"Authorization": "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=838d461dd62852877565b9f91558a9da26d7af50d8fadf3c48cc1a9f6d3561f4",
475475
"x-amz-date": "20181011T150505Z",
476476
"x-amz-security-token": "security_token",
477477
},
@@ -682,7 +682,7 @@ async def test_aws_auth_query_reserved_with_fragment(httpx_mock: HTTPXMock):
682682
)
683683

684684
httpx_mock.add_response(
685-
url="https://authorized_only/?@#$%25%5E&+=/,?%3E%3C%60%22;:%5C%7C][%7B%7D%20=@#$%25%5E&+=/,?%3E%3C%60%22;:%5C%7C][%7B%7D",
685+
url=r'https://authorized_only/?@#$%^&+=/,?%3E%3C`";:\|][{}%20=@#$%^&+=/,?%3E%3C`";:\|][{}',
686686
method="POST",
687687
match_headers={
688688
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
@@ -760,7 +760,7 @@ async def test_aws_auth_path_quoting(httpx_mock: HTTPXMock):
760760
)
761761

762762
httpx_mock.add_response(
763-
url="https://authorized_only/test/hello-*.&%5E~+%7B%7D!$%C2%A3_%20",
763+
url="https://authorized_only/test/hello-*.&^~+{}!$%C2%A3_%20",
764764
method="POST",
765765
match_headers={
766766
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
@@ -784,7 +784,7 @@ async def test_aws_auth_path_percent_encode_non_s3(httpx_mock: HTTPXMock):
784784
)
785785

786786
httpx_mock.add_response(
787-
url="https://authorized_only/test/%2a%2b%25/~-_%5E&%20%25%25",
787+
url="https://authorized_only/test/%2a%2b%25/~-_^&%20%%",
788788
method="POST",
789789
match_headers={
790790
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
@@ -808,7 +808,7 @@ async def test_aws_auth_path_percent_encode_s3(httpx_mock: HTTPXMock):
808808
)
809809

810810
httpx_mock.add_response(
811-
url="https://authorized_only/test/%2a%2b%25/~-_%5E&%20%25%25",
811+
url="https://authorized_only/test/%2a%2b%25/~-_^&%20%%",
812812
method="POST",
813813
match_headers={
814814
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",

0 commit comments

Comments
 (0)