Skip to content

Commit

Permalink
Merge pull request #107 from Colin-b/develop
Browse files Browse the repository at this point in the history
Release 0.23.0
  • Loading branch information
Colin-b authored Jan 7, 2025
2 parents 3fd7c07 + 0d9a3ac commit f4c70b2
Show file tree
Hide file tree
Showing 34 changed files with 1,040 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: '3.13'
- name: Create packages
run: |
python -m pip install build
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v4
Expand All @@ -26,4 +26,13 @@ jobs:
- name: Create packages
run: |
python -m pip install build
python -m build .
python -m build .
rm -Rf httpx_auth
- name: Install wheel
run: |
python -m pip install dist/httpx_auth-0.23.0-py3-none-any.whl --force-reinstall
python -c 'import httpx_auth'
- name: Install source distribution
run: |
python -m pip install dist/httpx_auth-0.23.0.tar.gz --force-reinstall
python -c 'import httpx_auth'
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/psf/black
rev: 24.1.1
rev: 24.8.0
hooks:
- id: black
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.23.0] - 2025-01-07
### Fixed
- Bearer tokens with nested JSON string are now properly handled. Thanks to [`Patrick Rodrigues`](https://github.com/pythrick).
- 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.

### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.28.\*
- Exceptions issued by `httpx_auth` are now inheriting from `httpx_auth.HttpxAuthException`, itself inheriting from `httpx.HTTPError`, instead of `Exception`.

### Added
- Explicit support for python `3.13`.

## [0.22.0] - 2024-03-02
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.27.\*
Expand Down Expand Up @@ -250,7 +262,8 @@ Note that a few changes were made:
### Added
- Placeholder for port of requests_auth to httpx

[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.22.0...HEAD
[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.23.0...HEAD
[0.23.0]: https://github.com/Colin-b/httpx_auth/compare/v0.22.0...v0.23.0
[0.22.0]: https://github.com/Colin-b/httpx_auth/compare/v0.21.0...v0.22.0
[0.21.0]: https://github.com/Colin-b/httpx_auth/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/Colin-b/httpx_auth/compare/v0.19.0...v0.20.0
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
<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>
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<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>
<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>
<a href="https://pypi.org/project/httpx-auth/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/httpx_auth"></a>
</p>

> [!NOTE]
> Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0).
>
> However, current state can be considered as stable.
Expand Down Expand Up @@ -376,7 +377,7 @@ Note:
| `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 |
| `client` | `httpx.Client` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |

Any other parameter will be put as body parameters in the token URL.
Any other parameter will be put as body parameters in the token URL.


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

### Managing the web browser

#### Authentication response pages

You can configure the browser display settings thanks to `httpx_auth.OAuth2.display` as in the following:
```python
from httpx_auth import OAuth2, DisplaySettings
Expand All @@ -727,6 +730,16 @@ The following parameters can be provided to `DisplaySettings`:
| `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 |
| `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. | |

#### Text-mode web browser

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.

```bash
BROWSER="/usr/bin/links %s &"
```

For more information, please see the implementation of [`webbrowser.get()`][5].

## AWS Signature v4

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).
Expand Down Expand Up @@ -996,4 +1009,6 @@ def test_something(browser_mock: BrowserMock):
[1]: https://pypi.python.org/pypi/httpx "httpx module"
[2]: https://www.python-httpx.org/advanced/#customizing-authentication "authentication parameter on httpx module"
[3]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken "OpenID ID Token specifications"
[4]: https://docs.python.org/3/library/webbrowser.html#webbrowser.open "Python webbrowser module"
[5]: https://github.com/python/cpython/blob/main/Lib/webbrowser.py "Python webbrowser module code"
[6]: https://docs.pytest.org/en/latest/ "pytest module"
2 changes: 2 additions & 0 deletions httpx_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
InvalidToken,
TokenExpiryNotProvided,
InvalidGrantRequest,
HttpxAuthException,
)
from httpx_auth.version import __version__

Expand Down Expand Up @@ -67,6 +68,7 @@
"JsonTokenFileCache",
"TokenMemoryCache",
"AWS4Auth",
"HttpxAuthException",
"GrantNotProvided",
"TimeoutOccurred",
"AuthenticationFailed",
Expand Down
33 changes: 19 additions & 14 deletions httpx_auth/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,42 @@
import httpx


class AuthenticationFailed(Exception):
class HttpxAuthException(httpx.HTTPError): ...


class AuthenticationFailed(HttpxAuthException):
"""User was not authenticated."""

def __init__(self):
Exception.__init__(self, "User was not authenticated.")
HttpxAuthException.__init__(self, "User was not authenticated.")


class TimeoutOccurred(Exception):
class TimeoutOccurred(HttpxAuthException):
"""No response within timeout interval."""

def __init__(self, timeout: float):
Exception.__init__(
HttpxAuthException.__init__(
self, f"User authentication was not received within {timeout} seconds."
)


class InvalidToken(Exception):
class InvalidToken(HttpxAuthException):
"""Token is invalid."""

def __init__(self, token_name: str):
Exception.__init__(self, f"{token_name} is invalid.")
HttpxAuthException.__init__(self, f"{token_name} is invalid.")


class GrantNotProvided(Exception):
class GrantNotProvided(HttpxAuthException):
"""Grant was not provided."""

def __init__(self, grant_name: str, dictionary_without_grant: dict):
Exception.__init__(
HttpxAuthException.__init__(
self, f"{grant_name} not provided within {dictionary_without_grant}."
)


class InvalidGrantRequest(Exception):
class InvalidGrantRequest(HttpxAuthException):
"""
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
"""
Expand Down Expand Up @@ -64,7 +67,7 @@ class InvalidGrantRequest(Exception):
}

def __init__(self, response: Union[httpx.Response, dict]):
Exception.__init__(self, InvalidGrantRequest.to_message(response))
HttpxAuthException.__init__(self, InvalidGrantRequest.to_message(response))

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


class StateNotProvided(Exception):
class StateNotProvided(HttpxAuthException):
"""State was not provided."""

def __init__(self, dictionary_without_state: dict):
Exception.__init__(
HttpxAuthException.__init__(
self, f"state not provided within {dictionary_without_state}."
)


class TokenExpiryNotProvided(Exception):
class TokenExpiryNotProvided(HttpxAuthException):
"""Token expiry was not provided."""

def __init__(self, token_body: dict):
Exception.__init__(self, f"Expiry (exp) is not provided in {token_body}.")
HttpxAuthException.__init__(
self, f"Expiry (exp) is not provided in {token_body}."
)
6 changes: 5 additions & 1 deletion httpx_auth/_oauth2/client_credentials.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
from hashlib import sha512
from typing import Union, Iterable

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

all_parameters_in_url = _add_parameters(self.token_url, self.data)
cache_data = copy.deepcopy(self.data)
cache_data["_httpx_auth_client_id"] = self.client_id
cache_data["_httpx_auth_client_secret"] = self.client_secret
all_parameters_in_url = _add_parameters(self.token_url, cache_data)
state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest()

super().__init__(
Expand Down
2 changes: 1 addition & 1 deletion httpx_auth/_oauth2/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def decode_base64(base64_encoded_string: str) -> str:
missing_padding = len(base64_encoded_string) % 4
if missing_padding != 0:
base64_encoded_string += "=" * (4 - missing_padding)
return base64.b64decode(base64_encoded_string).decode("unicode_escape")
return base64.urlsafe_b64decode(base64_encoded_string).decode("utf-8")


def is_expired(expiry: float, early_expiry: float) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion httpx_auth/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
__version__ = "0.22.0"
__version__ = "0.23.0"
16 changes: 8 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ classifiers=[
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Build Tools",
]
dependencies = [
"httpx==0.27.*",
"httpx==0.28.*",
]
dynamic = ["version"]

Expand All @@ -45,22 +46,21 @@ testing = [
# Used to generate test tokens
"pyjwt==2.*",
# Used to mock httpx
"pytest_httpx==0.30.*",
"pytest_httpx==0.35.*",
# Used to mock date and time
"time-machine==2.*",
# Used to check coverage
"pytest-cov==4.*",
"pytest-cov==6.*",
# Used to run async tests
"pytest-asyncio==0.23.*",
"pytest-asyncio==0.25.*",
]

[tool.setuptools.packages.find]
exclude = ["tests*"]

[tool.setuptools.dynamic]
version = {attr = "httpx_auth.version.__version__"}

[tool.pytest.ini_options]
filterwarnings = [
"error",
]
]
# Silence deprecation warnings about option "asyncio_default_fixture_loop_scope"
asyncio_default_fixture_loop_scope = "function"
16 changes: 8 additions & 8 deletions tests/aws_signature_v4/test_aws4auth_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ async def test_aws_auth_with_content_in_request(httpx_mock: HTTPXMock):
method="POST",
match_json=[{"key": "value"}],
match_headers={
"x-amz-content-sha256": "fb65c1441d6743274738fe3b3042a73167ba1fb2d34679d8dd16433473758f97",
"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",
"x-amz-content-sha256": "1e1d3e3fb0bcfb7b2b61f687369d0227e6aefd6739e1182312382ab03e83b75f",
"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",
"x-amz-date": "20181011T150505Z",
},
)
Expand Down Expand Up @@ -470,8 +470,8 @@ async def test_aws_auth_with_security_token_and_content_in_request(
method="POST",
match_json=[{"key": "value"}],
match_headers={
"x-amz-content-sha256": "fb65c1441d6743274738fe3b3042a73167ba1fb2d34679d8dd16433473758f97",
"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",
"x-amz-content-sha256": "1e1d3e3fb0bcfb7b2b61f687369d0227e6aefd6739e1182312382ab03e83b75f",
"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",
"x-amz-date": "20181011T150505Z",
"x-amz-security-token": "security_token",
},
Expand Down Expand Up @@ -682,7 +682,7 @@ async def test_aws_auth_query_reserved_with_fragment(httpx_mock: HTTPXMock):
)

httpx_mock.add_response(
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",
url=r'https://authorized_only/?@#$%^&+=/,?%3E%3C`";:\|][{}%20=@#$%^&+=/,?%3E%3C`";:\|][{}',
method="POST",
match_headers={
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Expand Down Expand Up @@ -760,7 +760,7 @@ async def test_aws_auth_path_quoting(httpx_mock: HTTPXMock):
)

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

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

httpx_mock.add_response(
url="https://authorized_only/test/%2a%2b%25/~-_%5E&%20%25%25",
url="https://authorized_only/test/%2a%2b%25/~-_^&%20%%",
method="POST",
match_headers={
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Expand Down
Loading

0 comments on commit f4c70b2

Please sign in to comment.