From 6ea73c61043349cf139fdcfb206aeda36274b632 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:43:37 +0000 Subject: [PATCH 1/8] Bump aiohttp from 3.12.14 to 3.14.3 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.12.14 to 3.14.3. - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.12.14...v3.14.3) --- updated-dependencies: - dependency-name: aiohttp dependency-version: 3.13.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9dd0d92..bac5356 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,6 @@ pytest<9.0.0 flask>=2.0.3,<4.0.0 Django>=3.2.9,<5.0.0 atlassian-httptest==1.0.0 -aiohttp==3.12.14 +aiohttp==3.14.3 types-requests types-setuptools From c1544a1acc7873105b0d99b8c8c8d379ca2342eb Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 25 Aug 2026 21:58:12 +1000 Subject: [PATCH 2/8] Sem-ver: bugfix Re-raise the exception inside of handle_retrieval_exception Signed-off-by: David Black --- atlassian_jwt_auth/key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian_jwt_auth/key.py b/atlassian_jwt_auth/key.py index 9cb80b8..89c5a45 100644 --- a/atlassian_jwt_auth/key.py +++ b/atlassian_jwt_auth/key.py @@ -153,7 +153,7 @@ def handle_retrieval_exception( if isinstance(original_exception, ConnectionError): return if exception.status_code is None or exception.status_code < 500: - raise + raise exception def retrieve( self, key_identifier: Union[KeyIdentifier, str], **requests_kwargs: Any From 0aafe40e4d500399804bb12d696c5b5e1260b1b4 Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 25 Aug 2026 21:58:45 +1000 Subject: [PATCH 3/8] Sem-ver bugfix: Update config for ruff Signed-off-by: David Black --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e12922e..e8f4e1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ line-ending = "auto" [tool.ruff.lint] extend-select = ["I"] +ignore = ["UP045", "UP007", "UP032", "C405", "SIM118", "C408", "RET501", "PIE808", "UP024", "UP031"] [tool.mypy] warn_unused_configs = true From 3cac250a0f26431d62d50ba3c54bd651a122ed3e Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 25 Aug 2026 22:07:58 +1000 Subject: [PATCH 4/8] Sem-ver bugfix: fix ruff issues Signed-off-by: David Black --- atlassian_jwt_auth/__init__.py | 4 ++-- atlassian_jwt_auth/algorithms.py | 5 +---- atlassian_jwt_auth/auth.py | 7 +++---- .../contrib/aiohttp/__init__.py | 2 +- atlassian_jwt_auth/contrib/aiohttp/auth.py | 3 ++- atlassian_jwt_auth/contrib/aiohttp/key.py | 9 +++++---- .../contrib/aiohttp/verifier.py | 5 +++-- atlassian_jwt_auth/contrib/requests.py | 5 ++--- .../contrib/tests/aiohttp/test_auth.py | 4 ++-- .../tests/aiohttp/test_public_key_provider.py | 2 +- .../contrib/tests/test_requests.py | 2 +- atlassian_jwt_auth/contrib/tests/utils.py | 8 ++++---- atlassian_jwt_auth/exceptions.py | 10 ++++------ atlassian_jwt_auth/frameworks/common/asap.py | 5 +++-- .../frameworks/common/backend.py | 9 +++++---- .../frameworks/common/decorators.py | 5 +++-- .../frameworks/django/__init__.py | 6 +++--- .../frameworks/django/decorators.py | 3 ++- .../frameworks/django/middleware.py | 5 +++-- .../frameworks/django/tests/test_django.py | 8 ++++---- .../frameworks/flask/__init__.py | 2 +- .../frameworks/flask/decorators.py | 4 ++-- .../frameworks/wsgi/middleware.py | 2 +- .../frameworks/wsgi/tests/test_wsgi.py | 18 +++++++++--------- atlassian_jwt_auth/key.py | 19 ++++++++++--------- atlassian_jwt_auth/signer.py | 15 +++++++-------- .../tests/test_private_key_provider.py | 2 +- .../tests/test_public_key_provider.py | 14 +++++++------- atlassian_jwt_auth/tests/test_signer.py | 12 ++++++++---- .../tests/test_signer_private_key_repo.py | 2 +- atlassian_jwt_auth/tests/test_verifier.py | 4 ++-- atlassian_jwt_auth/tests/utils.py | 5 +++-- atlassian_jwt_auth/verifier.py | 9 +++++---- setup.py | 0 34 files changed, 111 insertions(+), 104 deletions(-) mode change 100644 => 100755 setup.py diff --git a/atlassian_jwt_auth/__init__.py b/atlassian_jwt_auth/__init__.py index 1041b98..9ba2ccd 100644 --- a/atlassian_jwt_auth/__init__.py +++ b/atlassian_jwt_auth/__init__.py @@ -10,10 +10,10 @@ from atlassian_jwt_auth.verifier import JWTAuthVerifier __all__ = [ - "get_permitted_algorithm_names", "HTTPSPublicKeyRetriever", + "JWTAuthVerifier", "KeyIdentifier", "create_signer", "create_signer_from_file_private_key_repository", - "JWTAuthVerifier", + "get_permitted_algorithm_names", ] diff --git a/atlassian_jwt_auth/algorithms.py b/atlassian_jwt_auth/algorithms.py index df30ffc..44b821b 100644 --- a/atlassian_jwt_auth/algorithms.py +++ b/atlassian_jwt_auth/algorithms.py @@ -1,7 +1,4 @@ -from typing import List - - -def get_permitted_algorithm_names() -> List[str]: +def get_permitted_algorithm_names() -> list[str]: """returns permitted algorithm names.""" return [ "RS256", diff --git a/atlassian_jwt_auth/auth.py b/atlassian_jwt_auth/auth.py index 0cec861..aaf5a7d 100644 --- a/atlassian_jwt_auth/auth.py +++ b/atlassian_jwt_auth/auth.py @@ -1,13 +1,12 @@ -from __future__ import absolute_import - -from typing import Any, Iterable, Union +from collections.abc import Iterable +from typing import Any, Union import atlassian_jwt_auth from atlassian_jwt_auth import KeyIdentifier from atlassian_jwt_auth.signer import JWTAuthSigner -class BaseJWTAuth(object): +class BaseJWTAuth: """Adds a JWT bearer token to the request per the ASAP specification""" def __init__( diff --git a/atlassian_jwt_auth/contrib/aiohttp/__init__.py b/atlassian_jwt_auth/contrib/aiohttp/__init__.py index 916c28f..0725cd0 100644 --- a/atlassian_jwt_auth/contrib/aiohttp/__init__.py +++ b/atlassian_jwt_auth/contrib/aiohttp/__init__.py @@ -5,7 +5,7 @@ from .verifier import JWTAuthVerifier __all__ = [ - "JWTAuth", "HTTPSPublicKeyRetriever", + "JWTAuth", "JWTAuthVerifier", ] diff --git a/atlassian_jwt_auth/contrib/aiohttp/auth.py b/atlassian_jwt_auth/contrib/aiohttp/auth.py index a1aa7aa..60af14c 100644 --- a/atlassian_jwt_auth/contrib/aiohttp/auth.py +++ b/atlassian_jwt_auth/contrib/aiohttp/auth.py @@ -1,4 +1,5 @@ -from typing import Any, Iterable, Union +from collections.abc import Iterable +from typing import Any, Union from aiohttp import BasicAuth diff --git a/atlassian_jwt_auth/contrib/aiohttp/key.py b/atlassian_jwt_auth/contrib/aiohttp/key.py index fc96ef8..d0ef71c 100644 --- a/atlassian_jwt_auth/contrib/aiohttp/key.py +++ b/atlassian_jwt_auth/contrib/aiohttp/key.py @@ -1,7 +1,8 @@ import asyncio import urllib.parse from asyncio import AbstractEventLoop -from typing import Any, Awaitable, Dict, Optional +from collections.abc import Awaitable +from typing import Any, Optional import aiohttp @@ -31,8 +32,8 @@ def _get_session(self) -> aiohttp.ClientSession: # type: ignore[override] return HTTPSPublicKeyRetriever._class_session def _convert_proxies_to_proxy_arg( - self, url: str, requests_kwargs: Dict[Any, Any] - ) -> Dict[str, Any]: + self, url: str, requests_kwargs: dict[Any, Any] + ) -> dict[str, Any]: """returns a modified requests_kwargs dict that contains proxy information in a form that aiohttp accepts (it wants proxy information instead of a dict of proxies). @@ -46,7 +47,7 @@ def _convert_proxies_to_proxy_arg( return requests_kwargs async def _retrieve( - self, url: str, requests_kwargs: Dict[Any, Any] + self, url: str, requests_kwargs: dict[Any, Any] ) -> Awaitable[str]: requests_kwargs = self._convert_proxies_to_proxy_arg(url, requests_kwargs) try: diff --git a/atlassian_jwt_auth/contrib/aiohttp/verifier.py b/atlassian_jwt_auth/contrib/aiohttp/verifier.py index e7aad9e..39a3c04 100644 --- a/atlassian_jwt_auth/contrib/aiohttp/verifier.py +++ b/atlassian_jwt_auth/contrib/aiohttp/verifier.py @@ -1,5 +1,6 @@ import asyncio -from typing import Any, Dict, Iterable, Union +from collections.abc import Iterable +from typing import Any, Union import jwt @@ -14,7 +15,7 @@ async def verify_jwt( # type: ignore[override] audience: Union[str, Iterable[str]], leeway: int = 0, **requests_kwargs: Any, - ) -> Dict[Any, Any]: + ) -> dict[Any, Any]: """Verify if the token is correct Returns: diff --git a/atlassian_jwt_auth/contrib/requests.py b/atlassian_jwt_auth/contrib/requests.py index 9a2bba1..65c7981 100644 --- a/atlassian_jwt_auth/contrib/requests.py +++ b/atlassian_jwt_auth/contrib/requests.py @@ -1,6 +1,5 @@ -from __future__ import absolute_import - -from typing import Any, Iterable, Union +from collections.abc import Iterable +from typing import Any, Union import requests from requests.auth import AuthBase diff --git a/atlassian_jwt_auth/contrib/tests/aiohttp/test_auth.py b/atlassian_jwt_auth/contrib/tests/aiohttp/test_auth.py index 421c9a9..757e7f6 100644 --- a/atlassian_jwt_auth/contrib/tests/aiohttp/test_auth.py +++ b/atlassian_jwt_auth/contrib/tests/aiohttp/test_auth.py @@ -1,5 +1,5 @@ import unittest -from typing import Any, Type +from typing import Any from atlassian_jwt_auth.auth import BaseJWTAuth from atlassian_jwt_auth.contrib.aiohttp.auth import JWTAuth, create_jwt_auth @@ -10,7 +10,7 @@ class BaseAuthTest(test_requests.BaseRequestsTest): """tests for the contrib.aiohttp.JWTAuth class""" - auth_cls: Type[JWTAuth] = JWTAuth + auth_cls: type[JWTAuth] = JWTAuth def _get_auth_header(self, auth) -> bytes: return auth.encode().encode("latin1") diff --git a/atlassian_jwt_auth/contrib/tests/aiohttp/test_public_key_provider.py b/atlassian_jwt_auth/contrib/tests/aiohttp/test_public_key_provider.py index 649513c..b26480a 100644 --- a/atlassian_jwt_auth/contrib/tests/aiohttp/test_public_key_provider.py +++ b/atlassian_jwt_auth/contrib/tests/aiohttp/test_public_key_provider.py @@ -33,7 +33,7 @@ def _get_session(self) -> Mock: return session -class BaseHTTPSPublicKeyRetrieverTestMixin(object): +class BaseHTTPSPublicKeyRetrieverTestMixin: """Tests for aiohttp.HTTPSPublicKeyRetriever class for RS256 algorithm""" def setUp(self): diff --git a/atlassian_jwt_auth/contrib/tests/test_requests.py b/atlassian_jwt_auth/contrib/tests/test_requests.py index 7d23fd9..417164c 100644 --- a/atlassian_jwt_auth/contrib/tests/test_requests.py +++ b/atlassian_jwt_auth/contrib/tests/test_requests.py @@ -10,7 +10,7 @@ from atlassian_jwt_auth.tests import utils -class BaseRequestsTest(object): +class BaseRequestsTest: """tests for the contrib.requests.JWTAuth class""" auth_cls: Any = JWTAuth diff --git a/atlassian_jwt_auth/contrib/tests/utils.py b/atlassian_jwt_auth/contrib/tests/utils.py index f7d5113..33a5ac4 100644 --- a/atlassian_jwt_auth/contrib/tests/utils.py +++ b/atlassian_jwt_auth/contrib/tests/utils.py @@ -1,17 +1,17 @@ -from typing import Any, Dict, Type +from typing import Any import atlassian_jwt_auth from atlassian_jwt_auth import JWTAuthVerifier from atlassian_jwt_auth.key import BasePublicKeyRetriever -def get_static_retriever_class(keys: Dict[str, Any]) -> Type[BasePublicKeyRetriever]: +def get_static_retriever_class(keys: dict[str, Any]) -> type[BasePublicKeyRetriever]: class StaticPublicKeyRetriever(BasePublicKeyRetriever): """Retrieves a key from a static dict of public keys (for use in tests only)""" def __init__(self, *args: Any, **kwargs: Any) -> None: - self.keys: Dict[str, Any] = keys + self.keys: dict[str, Any] = keys def retrieve(self, key_identifier, **requests_kwargs) -> Any: return self.keys[key_identifier.key_id] @@ -19,5 +19,5 @@ def retrieve(self, key_identifier, **requests_kwargs) -> Any: return StaticPublicKeyRetriever -def static_verifier(keys: Dict[str, Any]) -> JWTAuthVerifier: +def static_verifier(keys: dict[str, Any]) -> JWTAuthVerifier: return atlassian_jwt_auth.JWTAuthVerifier(get_static_retriever_class(keys)()) diff --git a/atlassian_jwt_auth/exceptions.py b/atlassian_jwt_auth/exceptions.py index d831e0f..d7cdeae 100644 --- a/atlassian_jwt_auth/exceptions.py +++ b/atlassian_jwt_auth/exceptions.py @@ -1,7 +1,7 @@ from typing import Any -class _WrappedException(object): +class _WrappedException: """Allow wrapping exceptions in a new class while preserving the original as an attribute. @@ -18,10 +18,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: if isinstance(orig, Exception): wrapped_args[0] = str(orig) self.original_exception = getattr(orig, "original_exception", orig) - super(_WrappedException, self).__init__(*wrapped_args, **kwargs) + super().__init__(*wrapped_args, **kwargs) -class _WithStatus(object): +class _WithStatus: """Allow an optional status_code attribute on wrapped exceptions. This should allow inspecting HTTP-related errors without having to know @@ -30,7 +30,7 @@ class _WithStatus(object): def __init__(self, *args: Any, **kwargs: Any) -> None: status_code = kwargs.pop("status_code", None) - super(_WithStatus, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.status_code = status_code @@ -64,5 +64,3 @@ class SubjectDoesNotMatchIssuerException(ASAPAuthenticationException): class NoTokenProvidedError(ASAPAuthenticationException): """Raise when no token is provided""" - - pass diff --git a/atlassian_jwt_auth/frameworks/common/asap.py b/atlassian_jwt_auth/frameworks/common/asap.py index 9a54fc1..18b78a0 100644 --- a/atlassian_jwt_auth/frameworks/common/asap.py +++ b/atlassian_jwt_auth/frameworks/common/asap.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Dict, Iterable, Optional +from collections.abc import Iterable +from typing import Any, Optional from jwt.exceptions import InvalidIssuerError, InvalidTokenError @@ -90,7 +91,7 @@ def _process_asap_token( def _verify_issuers( - asap_claims: Dict[Any, Any], issuers: Optional[Iterable[str]] = None + asap_claims: dict[Any, Any], issuers: Optional[Iterable[str]] = None ) -> None: """Verify that the issuer in the claims is valid and is expected.""" claim_iss = asap_claims.get("iss") diff --git a/atlassian_jwt_auth/frameworks/common/backend.py b/atlassian_jwt_auth/frameworks/common/backend.py index 1f8359d..82c48bf 100644 --- a/atlassian_jwt_auth/frameworks/common/backend.py +++ b/atlassian_jwt_auth/frameworks/common/backend.py @@ -1,6 +1,7 @@ +import typing from abc import ABCMeta, abstractmethod, abstractproperty from functools import lru_cache -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union from atlassian_jwt_auth import HTTPSPublicKeyRetriever, JWTAuthVerifier @@ -34,8 +35,8 @@ class Backend: __metaclass__ = ABCMeta - default_headers_401 = {"WWW-Authenticate": "Bearer"} - default_settings = { + default_headers_401: typing.ClassVar = {"WWW-Authenticate": "Bearer"} + default_settings: typing.ClassVar = { # The class to be instantiated to retrieve public keys "ASAP_KEY_RETRIEVER_CLASS": HTTPSPublicKeyRetriever, # The repository URL where the key retriever can fetch public keys @@ -117,7 +118,7 @@ def get_verifier(self, settings: Optional[SettingsDict] = None) -> JWTAuthVerifi def _get_verifier(self, settings: SettingsDict) -> JWTAuthVerifier: return _get_verifier(settings) - def _process_settings(self, settings: Union[SettingsDict, Dict]) -> SettingsDict: + def _process_settings(self, settings: Union[SettingsDict, dict]) -> SettingsDict: valid_issuers = settings.get("ASAP_VALID_ISSUERS") if valid_issuers: settings["ASAP_VALID_ISSUERS"] = set(valid_issuers) diff --git a/atlassian_jwt_auth/frameworks/common/decorators.py b/atlassian_jwt_auth/frameworks/common/decorators.py index 2b2d6ad..eb3e063 100644 --- a/atlassian_jwt_auth/frameworks/common/decorators.py +++ b/atlassian_jwt_auth/frameworks/common/decorators.py @@ -1,5 +1,6 @@ +from collections.abc import Callable, Iterable from functools import wraps -from typing import Any, Callable, Dict, Iterable, Optional +from typing import Any, Optional from jwt.exceptions import InvalidIssuerError, InvalidTokenError @@ -103,7 +104,7 @@ def restrict_asap_wrapper(request, *args, **kwargs) -> Any: def _update_settings_from_kwargs( - settings: Dict[Any, Any], + settings: dict[Any, Any], issuers: Optional[Iterable] = None, required: bool = True, subject_should_match_issuer: Optional[bool] = None, diff --git a/atlassian_jwt_auth/frameworks/django/__init__.py b/atlassian_jwt_auth/frameworks/django/__init__.py index 0f5cc56..f946b16 100644 --- a/atlassian_jwt_auth/frameworks/django/__init__.py +++ b/atlassian_jwt_auth/frameworks/django/__init__.py @@ -2,9 +2,9 @@ from .middleware import OldStyleASAPMiddleware, asap_middleware __all__ = [ - "restrict_asap", - "with_asap", - "requires_asap", "OldStyleASAPMiddleware", "asap_middleware", + "requires_asap", + "restrict_asap", + "with_asap", ] diff --git a/atlassian_jwt_auth/frameworks/django/decorators.py b/atlassian_jwt_auth/frameworks/django/decorators.py index 1ff6d2b..3654db4 100644 --- a/atlassian_jwt_auth/frameworks/django/decorators.py +++ b/atlassian_jwt_auth/frameworks/django/decorators.py @@ -1,4 +1,5 @@ -from typing import Callable, Iterable, Optional +from collections.abc import Callable, Iterable +from typing import Optional from ..common.backend import Backend from ..common.decorators import _restrict_asap, _with_asap diff --git a/atlassian_jwt_auth/frameworks/django/middleware.py b/atlassian_jwt_auth/frameworks/django/middleware.py index c87fa08..cbf400c 100644 --- a/atlassian_jwt_auth/frameworks/django/middleware.py +++ b/atlassian_jwt_auth/frameworks/django/middleware.py @@ -1,4 +1,5 @@ -from typing import Any, Callable, Optional +from collections.abc import Callable +from typing import Any, Optional from django.http import HttpRequest @@ -24,7 +25,7 @@ def middleware(request: HttpRequest) -> Any: return middleware -class OldStyleASAPMiddleware(object): +class OldStyleASAPMiddleware: """Middleware to enable ASAP for all requests (for legacy applications using MIDDLEWARE_CLASSES)""" diff --git a/atlassian_jwt_auth/frameworks/django/tests/test_django.py b/atlassian_jwt_auth/frameworks/django/tests/test_django.py index 1f3e6f7..7365150 100644 --- a/atlassian_jwt_auth/frameworks/django/tests/test_django.py +++ b/atlassian_jwt_auth/frameworks/django/tests/test_django.py @@ -19,7 +19,7 @@ ) -class DjangoAsapMixin(object): +class DjangoAsapMixin: @classmethod def setUpClass(cls): os.environ.setdefault( @@ -28,15 +28,15 @@ def setUpClass(cls): ) django.setup() - super(DjangoAsapMixin, cls).setUpClass() + super().setUpClass() @classmethod def tearDownClass(cls): - super(DjangoAsapMixin, cls).tearDownClass() + super().tearDownClass() del os.environ["DJANGO_SETTINGS_MODULE"] def setUp(self): - super(DjangoAsapMixin, self).setUp() + super().setUp() self._private_key_pem = self.get_new_private_key_in_pem_format() self._public_key_pem = utils.get_public_key_pem_for_private_key_pem( self._private_key_pem diff --git a/atlassian_jwt_auth/frameworks/flask/__init__.py b/atlassian_jwt_auth/frameworks/flask/__init__.py index 6f64b31..1518200 100644 --- a/atlassian_jwt_auth/frameworks/flask/__init__.py +++ b/atlassian_jwt_auth/frameworks/flask/__init__.py @@ -1,3 +1,3 @@ from .decorators import requires_asap, with_asap -__all__ = ["with_asap", "requires_asap"] +__all__ = ["requires_asap", "with_asap"] diff --git a/atlassian_jwt_auth/frameworks/flask/decorators.py b/atlassian_jwt_auth/frameworks/flask/decorators.py index 08c6adf..c1e4afe 100644 --- a/atlassian_jwt_auth/frameworks/flask/decorators.py +++ b/atlassian_jwt_auth/frameworks/flask/decorators.py @@ -1,5 +1,5 @@ -from collections.abc import Callable -from typing import Iterable, Optional +from collections.abc import Callable, Iterable +from typing import Optional from ..common.decorators import _with_asap from .backend import FlaskBackend diff --git a/atlassian_jwt_auth/frameworks/wsgi/middleware.py b/atlassian_jwt_auth/frameworks/wsgi/middleware.py index 1d11a25..e62fef9 100644 --- a/atlassian_jwt_auth/frameworks/wsgi/middleware.py +++ b/atlassian_jwt_auth/frameworks/wsgi/middleware.py @@ -7,7 +7,7 @@ Request = namedtuple("Request", ["environ", "start_response"]) -class ASAPMiddleware(object): +class ASAPMiddleware: def __init__(self, handler: Any, settings: Any) -> None: self._next = handler self._backend = WSGIBackend(settings) diff --git a/atlassian_jwt_auth/frameworks/wsgi/tests/test_wsgi.py b/atlassian_jwt_auth/frameworks/wsgi/tests/test_wsgi.py index a4f82ce..2a7a03c 100644 --- a/atlassian_jwt_auth/frameworks/wsgi/tests/test_wsgi.py +++ b/atlassian_jwt_auth/frameworks/wsgi/tests/test_wsgi.py @@ -56,7 +56,7 @@ def test_request_with_valid_token_is_allowed(self): token = create_token( "client-app", "server-app", "client-app/key01", self._private_key_pem ) - body, resp_info, environ = self.send_request(token=token) + _body, resp_info, environ = self.send_request(token=token) self.assertEqual(resp_info["status"], "200 OK") self.assertIn("ATL_ASAP_CLAIMS", environ) @@ -66,11 +66,11 @@ def test_request_with_duplicate_jti_is_rejected_as_per_setting(self): "client-app", "server-app", "client-app/key01", self._private_key_pem ) application = self.get_app_with_middleware(self.config) - body, resp_info, environ = self.send_request( + _body, resp_info, _environ = self.send_request( token=token, application=application ) self.assertEqual(resp_info["status"], "200 OK") - body, resp_info, environ = self.send_request( + _body, resp_info, _environ = self.send_request( token=token, application=application ) self.assertEqual(resp_info["status"], "401 Unauthorized") @@ -80,11 +80,11 @@ def _assert_request_with_duplicate_jti_is_accepted(self): "client-app", "server-app", "client-app/key01", self._private_key_pem ) application = self.get_app_with_middleware(self.config) - body, resp_info, environ = self.send_request( + _body, resp_info, _environ = self.send_request( token=token, application=application ) self.assertEqual(resp_info["status"], "200 OK") - body, resp_info, environ = self.send_request( + _body, resp_info, _environ = self.send_request( token=token, application=application ) self.assertEqual(resp_info["status"], "200 OK") @@ -100,12 +100,12 @@ def test_request_with_invalid_audience_is_rejected(self): token = create_token( "client-app", "invalid-audience", "client-app/key01", self._private_key_pem ) - body, resp_info, environ = self.send_request(token=token) + _body, resp_info, environ = self.send_request(token=token) self.assertEqual(resp_info["status"], "401 Unauthorized") self.assertNotIn("ATL_ASAP_CLAIMS", environ) def test_request_with_invalid_token_is_rejected(self): - body, resp_info, environ = self.send_request(token=b"notavalidtoken") + _body, resp_info, environ = self.send_request(token=b"notavalidtoken") self.assertEqual(resp_info["status"], "401 Unauthorized") self.assertNotIn("ATL_ASAP_CLAIMS", environ) @@ -117,7 +117,7 @@ def test_request_subject_and_issue_not_matching(self): self._private_key_pem, subject="different", ) - body, resp_info, environ = self.send_request(token=token) + _body, resp_info, environ = self.send_request(token=token) self.assertEqual(resp_info["status"], "401 Unauthorized") self.assertNotIn("ATL_ASAP_CLAIMS", environ) @@ -130,6 +130,6 @@ def test_request_subject_does_not_need_to_match_issuer_from_settings(self): self._private_key_pem, subject="different", ) - body, resp_info, environ = self.send_request(token=token) + _body, resp_info, environ = self.send_request(token=token) self.assertEqual(resp_info["status"], "200 OK") self.assertIn("ATL_ASAP_CLAIMS", environ) diff --git a/atlassian_jwt_auth/key.py b/atlassian_jwt_auth/key.py index 89c5a45..c72ecaa 100644 --- a/atlassian_jwt_auth/key.py +++ b/atlassian_jwt_auth/key.py @@ -2,8 +2,9 @@ import logging import os import re +from collections.abc import Generator, Iterable from email.message import EmailMessage -from typing import Any, Generator, Iterable, Tuple, Union +from typing import Any, Union from urllib.parse import unquote_plus import cachecontrol @@ -23,7 +24,7 @@ PEM_FILE_TYPE = "application/x-pem-file" -class KeyIdentifier(object): +class KeyIdentifier: """This class represents a key identifier""" def __init__(self, identifier: str) -> None: @@ -58,7 +59,7 @@ def _get_key_id_from_jwt_header(a_jwt: Union[str, bytes]) -> KeyIdentifier: return KeyIdentifier(header["kid"]) -class BasePublicKeyRetriever(object): +class BasePublicKeyRetriever: """Base class for retrieving a public key.""" def retrieve(self, key_identifier: Union[KeyIdentifier, str], **kwargs) -> Any: @@ -174,10 +175,10 @@ def retrieve( raise PublicKeyRetrieverException("Cannot load key from key repositories") -class BasePrivateKeyRetriever(object): +class BasePrivateKeyRetriever: """This is the base private key retriever class.""" - def load(self, issuer: str) -> Tuple[Union[KeyIdentifier], Union[str, bytes]]: + def load(self, issuer: str) -> tuple[Union[KeyIdentifier], Union[str, bytes]]: """returns the key identifier and private key pem found for the given issuer. """ @@ -192,7 +193,7 @@ class DataUriPrivateKeyRetriever(BasePrivateKeyRetriever): def __init__(self, data_uri: str) -> None: self._data_uri = data_uri - def load(self, issuer: str) -> Tuple[Union[KeyIdentifier], Union[str, bytes]]: + def load(self, issuer: str) -> tuple[Union[KeyIdentifier], Union[str, bytes]]: if not self._data_uri.startswith("data:application/pkcs8;kid="): raise PrivateKeyRetrieverException("Unrecognised data uri format.") splitted = self._data_uri.split(";") @@ -227,7 +228,7 @@ def __init__( self.key_identifier: KeyIdentifier = key_identifier self.private_key_pem: Union[str, bytes] = private_key_pem - def load(self, issuer: str) -> Tuple[Union[KeyIdentifier], Union[str, bytes]]: + def load(self, issuer: str) -> tuple[Union[KeyIdentifier], Union[str, bytes]]: return self.key_identifier, self.private_key_pem @@ -242,7 +243,7 @@ def __init__(self, private_key_repository_path: str) -> None: private_key_repository_path ) - def load(self, issuer: str) -> Tuple[KeyIdentifier, str]: + def load(self, issuer: str) -> tuple[KeyIdentifier, str]: key_identifier = self._find_last_key_id(issuer) private_key_pem = self.private_key_repository.load_key(key_identifier) return key_identifier, private_key_pem @@ -256,7 +257,7 @@ def _find_last_key_id(self, issuer) -> KeyIdentifier: raise IOError("Issuer has no valid keys: %s" % issuer) -class FilePrivateKeyRepository(object): +class FilePrivateKeyRepository: """This class represents a file backed private key repository.""" def __init__(self, path) -> None: diff --git a/atlassian_jwt_auth/signer.py b/atlassian_jwt_auth/signer.py index cd9fb0c..2571299 100644 --- a/atlassian_jwt_auth/signer.py +++ b/atlassian_jwt_auth/signer.py @@ -1,7 +1,8 @@ import calendar import datetime import random -from typing import Any, Dict, Iterable, Optional, Union +from collections.abc import Iterable +from typing import Any, Optional, Union import jwt from cryptography.hazmat.backends import default_backend @@ -12,7 +13,7 @@ from atlassian_jwt_auth.key import BasePrivateKeyRetriever, KeyIdentifier -class JWTAuthSigner(object): +class JWTAuthSigner: def __init__( self, issuer: str, private_key_retriever: BasePrivateKeyRetriever, **kwargs: Any ) -> None: @@ -21,7 +22,7 @@ def __init__( self.lifetime = kwargs.get("lifetime", datetime.timedelta(minutes=1)) self.algorithm = kwargs.get("algorithm", "RS256") self.subject = kwargs.get("subject", None) - self._private_keys_cache: Dict[str, Any] = dict() + self._private_keys_cache: dict[str, Any] = dict() if self.algorithm not in set(algorithms.get_permitted_algorithm_names()): raise ValueError("Algorithm, '%s', is not permitted." % self.algorithm) @@ -54,7 +55,7 @@ def _obtain_private_key( def _generate_claims( self, audience: Union[str, Iterable[str]], **kwargs: Any - ) -> Dict[Any, Any]: + ) -> dict[Any, Any]: """returns a new dictionary of claims.""" now = self._now() claims = { @@ -94,9 +95,7 @@ class TokenReusingJWTAuthSigner(JWTAuthSigner): def __init__( self, issuer: str, private_key_retriever: BasePrivateKeyRetriever, **kwargs: Any ) -> None: - super(TokenReusingJWTAuthSigner, self).__init__( - issuer, private_key_retriever, **kwargs - ) + super().__init__(issuer, private_key_retriever, **kwargs) self.reuse_threshold = kwargs.get("reuse_jwt_threshold", 0.95) def get_cached_token( @@ -144,7 +143,7 @@ def generate_jwt(self, audience: Union[str, Iterable[str]], **kwargs: Any) -> by claims = self._generate_claims(audience, **kwargs) if existing_token and self.can_reuse_token(existing_token, claims): return existing_token - token = super(TokenReusingJWTAuthSigner, self).generate_jwt(audience, **kwargs) + token = super().generate_jwt(audience, **kwargs) self.set_cached_token(token) return token diff --git a/atlassian_jwt_auth/tests/test_private_key_provider.py b/atlassian_jwt_auth/tests/test_private_key_provider.py index 1e33c33..bcd9353 100644 --- a/atlassian_jwt_auth/tests/test_private_key_provider.py +++ b/atlassian_jwt_auth/tests/test_private_key_provider.py @@ -21,7 +21,7 @@ def convert_key_pem_format_to_der_format(private_key_pem): ) -class BaseDataUriPrivateKeyRetrieverTest(object): +class BaseDataUriPrivateKeyRetrieverTest: """tests for the DataUriPrivateKeyRetriever class.""" def setUp(self): diff --git a/atlassian_jwt_auth/tests/test_public_key_provider.py b/atlassian_jwt_auth/tests/test_public_key_provider.py index 5f71861..0509c98 100644 --- a/atlassian_jwt_auth/tests/test_public_key_provider.py +++ b/atlassian_jwt_auth/tests/test_public_key_provider.py @@ -26,7 +26,7 @@ def get_expected_and_os_proxies_dict(proxy_location): return expected_proxies, os_proxy_dict -class BaseHTTPSPublicKeyRetrieverTest(object): +class BaseHTTPSPublicKeyRetrieverTest: """tests for the HTTPSPublicKeyRetriever class.""" def create_retriever(self, url): @@ -169,13 +169,11 @@ class HTTPPublicKeyRetriever(HTTPSPublicKeyRetriever): def __init__(self, base_url): # pretend to the super class that this is an HTTPS url - super( - CachedHTTPPublicKeyRetrieverTest.HTTPPublicKeyRetriever, self - ).__init__(re.sub(r"^http", "https", base_url, flags=re.IGNORECASE)) + super().__init__(re.sub(r"^http", "https", base_url, flags=re.IGNORECASE)) self.base_url = base_url def setUp(self): - super(CachedHTTPPublicKeyRetrieverTest, self).setUp() + super().setUp() self._private_key_pem = self.get_new_private_key_in_pem_format() self._public_key_pem = utils.get_public_key_pem_for_private_key_pem( self._private_key_pem @@ -194,8 +192,10 @@ def wsgi(environ, start_response): ("content-type", "application/x-pem-file;charset=UTF-8"), ( "Cache-Control", - "public,max-age=300,stale-while-revalidate=" - "300,stale-if-error=300", + ( + "public,max-age=300,stale-while-revalidate=" + "300,stale-if-error=300" + ), ), ("Last-Modified", "Sun, 18 Jan 1970 18:14:21 GMT"), ], diff --git a/atlassian_jwt_auth/tests/test_signer.py b/atlassian_jwt_auth/tests/test_signer.py index c577456..26b9662 100644 --- a/atlassian_jwt_auth/tests/test_signer.py +++ b/atlassian_jwt_auth/tests/test_signer.py @@ -8,15 +8,17 @@ from atlassian_jwt_auth.tests import utils -class BaseJWTAuthSignerTest(object): +class BaseJWTAuthSignerTest: """tests for the JWTAuthSigner class.""" def setUp(self): self._private_key_pem = self.get_new_private_key_in_pem_format() - def test__generate_claims(self): + def test__generate_claims(self) -> None: """tests that _generate_claims works as expected.""" - expected_now = datetime.datetime(year=2001, day=1, month=1) + expected_now = datetime.datetime( + year=2001, day=1, month=1, tzinfo=datetime.timezone.utc + ) expected_audience = "example_aud" expected_iss = "eg" expected_key_id = "eg/ex" @@ -43,7 +45,9 @@ def test__generate_claims(self): def test_jti_changes(self): """tests that the jti of a claim changes.""" - expected_now = datetime.datetime(year=2001, day=1, month=1) + expected_now = datetime.datetime( + year=2001, day=1, month=1, tzinfo=datetime.timezone.utc + ) aud = "aud" jwt_auth_signer = utils.get_example_jwt_auth_signer( algorithm=self.algorithm, private_key_pem=self._private_key_pem diff --git a/atlassian_jwt_auth/tests/test_signer_private_key_repo.py b/atlassian_jwt_auth/tests/test_signer_private_key_repo.py index 9bb519b..8d36efd 100644 --- a/atlassian_jwt_auth/tests/test_signer_private_key_repo.py +++ b/atlassian_jwt_auth/tests/test_signer_private_key_repo.py @@ -8,7 +8,7 @@ from atlassian_jwt_auth.tests import utils -class BaseJWTAuthSignerWithFilePrivateKeyRetrieverTest(object): +class BaseJWTAuthSignerWithFilePrivateKeyRetrieverTest: """tests for the JWTAuthSigner using the FilePrivateKeyRetriever.""" def setUp(self): diff --git a/atlassian_jwt_auth/tests/test_verifier.py b/atlassian_jwt_auth/tests/test_verifier.py index 87aa9f5..ec8612b 100644 --- a/atlassian_jwt_auth/tests/test_verifier.py +++ b/atlassian_jwt_auth/tests/test_verifier.py @@ -20,7 +20,7 @@ class NoneAlgorithmJwtAuthSigner(atlassian_jwt_auth.signer.JWTAuthSigner): def generate_jwt(self, audience, **kwargs): alg_header = kwargs.get("alg_header", "none") - key_identifier, private_key_pem = self.private_key_retriever.load(self.issuer) + key_identifier, _ = self.private_key_retriever.load(self.issuer) return jwt.encode( self._generate_claims(audience, **kwargs), algorithm=None, @@ -29,7 +29,7 @@ def generate_jwt(self, audience, **kwargs): ) -class BaseJWTAuthVerifierTest(object): +class BaseJWTAuthVerifierTest: """tests for the JWTAuthVerifier class.""" def setUp(self): diff --git a/atlassian_jwt_auth/tests/utils.py b/atlassian_jwt_auth/tests/utils.py index bf60215..e444517 100644 --- a/atlassian_jwt_auth/tests/utils.py +++ b/atlassian_jwt_auth/tests/utils.py @@ -1,4 +1,5 @@ -from typing import Any, Iterable, Optional, Protocol, Union +from collections.abc import Iterable +from typing import Any, Optional, Protocol, Union from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization @@ -55,7 +56,7 @@ def create_token( return signer.generate_jwt(audience) -class BaseJWTAlgorithmTestMixin(object): +class BaseJWTAlgorithmTestMixin: """A mixin class to make testing different support for different jwt algorithms easier. """ diff --git a/atlassian_jwt_auth/verifier.py b/atlassian_jwt_auth/verifier.py index 2a5c50c..e8668e5 100644 --- a/atlassian_jwt_auth/verifier.py +++ b/atlassian_jwt_auth/verifier.py @@ -1,6 +1,7 @@ from collections import OrderedDict +from collections.abc import Iterable, Sequence from functools import lru_cache -from typing import Any, Dict, Iterable, Optional, Sequence, Union +from typing import Any, Optional, Union import jwt import jwt.api_jwt @@ -39,7 +40,7 @@ def _load_public_key( return alg_obj.prepare_key(public_key) -class JWTAuthVerifier(object): +class JWTAuthVerifier: """This class can be used to verify a JWT.""" def __init__( @@ -59,7 +60,7 @@ def verify_jwt( audience: str, leeway: int = 0, **requests_kwargs: Any, - ) -> Dict[Any, Any]: + ) -> dict[Any, Any]: """Verify if the token is correct Returns: @@ -95,7 +96,7 @@ def _decode_jwt( jwt_key: Union[AllowedPublicKeys, PyJWK, str, bytes], audience: Optional[Union[str, Iterable[str]]] = None, leeway: int = 0, - ) -> Dict[Any, Any]: + ) -> dict[Any, Any]: """Decode JWT and check if it's valid""" options: Options = Options( verify_signature=True, diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 From 150fa8646bf3e3f9b3bede7370106cf74892cfe9 Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 25 Aug 2026 22:09:55 +1000 Subject: [PATCH 5/8] Sem-ver: api-break Drop support for python 3.9 & 3.10 Signed-off-by: David Black --- .github/workflows/build.yml | 2 +- setup.cfg | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7509a0f..95b17c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] + python-version: [3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/setup.cfg b/setup.cfg index 5e7025e..edb9dc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,8 +17,6 @@ classifier = Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 From 1d9929abf8744cb8dbe180f8a46af7356d373dc5 Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 25 Aug 2026 22:10:51 +1000 Subject: [PATCH 6/8] Sem-ver: feature Add python 3.14 support Signed-off-by: David Black --- .github/workflows/build.yml | 8 ++++---- setup.cfg | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95b17c2..d391bdd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,15 +5,15 @@ on: [push] jobs: build: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest strategy: matrix: - python-version: [3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/setup.cfg b/setup.cfg index edb9dc5..935b987 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ classifier = Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 + Programming Language :: Python :: 3.14 License :: OSI Approved :: MIT License [files] From f458178c5ddf3ddd29666e053ff6e02eefedefe7 Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 25 Aug 2026 22:20:24 +1000 Subject: [PATCH 7/8] Sem-ver bugfix: Fix mypy issues Signed-off-by: David Black --- atlassian_jwt_auth/tests/test_signer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/atlassian_jwt_auth/tests/test_signer.py b/atlassian_jwt_auth/tests/test_signer.py index 26b9662..5b8aa40 100644 --- a/atlassian_jwt_auth/tests/test_signer.py +++ b/atlassian_jwt_auth/tests/test_signer.py @@ -25,7 +25,7 @@ def test__generate_claims(self) -> None: jwt_auth_signer = atlassian_jwt_auth.create_signer( expected_iss, expected_key_id, self._private_key_pem ) - jwt_auth_signer._now = lambda: expected_now + jwt_auth_signer._now = lambda: expected_now # type: ignore[method-assign] for additional_claims in [{}, {"extra": "thing"}]: expected_claims = { "iss": expected_iss, @@ -39,9 +39,9 @@ def test__generate_claims(self) -> None: claims = jwt_auth_signer._generate_claims( expected_audience, additional_claims=additional_claims ) - self.assertIsNotNone(claims["jti"]) + self.assertIsNotNone(claims["jti"]) # type: ignore[attr-defined] del claims["jti"] - self.assertEqual(claims, expected_claims) + self.assertEqual(claims, expected_claims) # type: ignore[attr-defined] def test_jti_changes(self): """tests that the jti of a claim changes.""" From 04ca84614f74fed8448c5d242a8d865746f3daef Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 25 Aug 2026 22:24:39 +1000 Subject: [PATCH 8/8] Sem-ver: bugfix Add permissions info to the github workflows Signed-off-by: David Black --- .github/workflows/build.yml | 3 +++ .github/workflows/codeql-analysis.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d391bdd..a13bc3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,7 @@ name: Tests +permissions: + contents: read + pull-requests: write on: [push] diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c8fa01e..4960b7c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -10,6 +10,9 @@ # supported CodeQL languages. # name: "CodeQL" +permissions: + contents: read + pull-requests: write on: push: