From 9e06d02576a8e4240a6055464dbbbbcd2b634dd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 00:40:41 +0000 Subject: [PATCH 1/3] fix: use PyJWK key algorithm when encoding without explicit algorithm (#1147) When a PyJWK object is passed to jwt.encode() without specifying an algorithm, the key's embedded algorithm is now used instead of defaulting to HS256. This is achieved by using a sentinel default value so the code can distinguish "no algorithm specified" from an explicit algorithm parameter. https://claude.ai/code/session_016Ekc2jQzpuiDpBvnMAMnUB --- jwt/api_jws.py | 11 +++++++++-- jwt/api_jwt.py | 4 ++-- tests/test_api_jws.py | 21 +++++++++++++++++++++ tests/test_api_jwt.py | 19 +++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 28da7a84..a320a543 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -27,6 +27,8 @@ from .algorithms import AllowedPrivateKeys, AllowedPublicKeys from .types import SigOptions +_ALGORITHM_UNSET = object() + class PyJWS: header_typ = "JWT" @@ -119,7 +121,7 @@ def encode( self, payload: bytes, key: AllowedPrivateKeys | PyJWK | str | bytes, - algorithm: str | None = "HS256", + algorithm: str | None = _ALGORITHM_UNSET, # type: ignore[assignment] headers: dict[str, Any] | None = None, json_encoder: type[json.JSONEncoder] | None = None, is_payload_detached: bool = False, @@ -128,7 +130,12 @@ def encode( segments: list[bytes] = [] # declare a new var to narrow the type for type checkers - if algorithm is None: + if algorithm is _ALGORITHM_UNSET: + if isinstance(key, PyJWK): + algorithm_ = key.algorithm_name + else: + algorithm_ = "HS256" + elif algorithm is None: if isinstance(key, PyJWK): algorithm_ = key.algorithm_name else: diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py index a26fa9c9..429c2d79 100644 --- a/jwt/api_jwt.py +++ b/jwt/api_jwt.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Union, cast -from .api_jws import PyJWS, _jws_global_obj +from .api_jws import PyJWS, _ALGORITHM_UNSET, _jws_global_obj from .exceptions import ( DecodeError, ExpiredSignatureError, @@ -91,7 +91,7 @@ def encode( self, payload: dict[str, Any], key: AllowedPrivateKeyTypes, - algorithm: str | None = "HS256", + algorithm: str | None = _ALGORITHM_UNSET, # type: ignore[assignment] headers: dict[str, Any] | None = None, json_encoder: type[json.JSONEncoder] | None = None, sort_headers: bool = True, diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py index 0f7e00dc..967d323b 100644 --- a/tests/test_api_jws.py +++ b/tests/test_api_jws.py @@ -261,6 +261,27 @@ def test_encode_with_jwk(self, jws: PyJWS, payload: bytes) -> None: ), } + def test_encode_with_jwk_uses_key_algorithm( + self, jws: PyJWS, payload: bytes + ) -> None: + """Test that encoding with a PyJWK key uses the key's algorithm + when no algorithm is explicitly specified. Regression test for #1147.""" + jwk = PyJWK( + { + "kty": "oct", + "alg": "HS384", + "k": "c2VjcmV0", # "secret" + } + ) + # Should use HS384 from the key, not default to HS256 + msg = jws.encode(payload, key=jwk) + header = jws.get_unverified_header(msg) + assert header["alg"] == "HS384" + + # Should also be decodable with the same key + decoded = jws.decode(msg, key=jwk) + assert decoded == payload + def test_decode_algorithm_param_should_be_case_sensitive(self, jws: PyJWS) -> None: example_jws = ( "eyJhbGciOiJoczI1NiIsInR5cCI6IkpXVCJ9" # alg = hs256 diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py index 02ee1657..c826a41e 100644 --- a/tests/test_api_jwt.py +++ b/tests/test_api_jwt.py @@ -7,6 +7,7 @@ import pytest from jwt.types import Options +from jwt.api_jwk import PyJWK from jwt.api_jwt import PyJWT from jwt.exceptions import ( DecodeError, @@ -45,6 +46,24 @@ def test_jwt_with_options(self) -> None: # assert that verify_signature is respected unless verify_exp is overridden assert jwt.options["verify_exp"] is False + def test_encode_with_jwk_uses_key_algorithm(self, jwt: PyJWT) -> None: + """Test that encoding with a PyJWK key uses the key's algorithm + when no algorithm is explicitly specified. Regression test for #1147.""" + jwk = PyJWK( + { + "kty": "oct", + "alg": "HS384", + "k": "c2VjcmV0", # "secret" + } + ) + payload = {"hello": "world"} + # Should use HS384 from the key, not default to HS256 + token = jwt.encode(payload, jwk) + header = jwt.decode_complete( + token, jwk, algorithms=["HS384"] + )["header"] + assert header["alg"] == "HS384" + def test_decodes_valid_jwt(self, jwt: PyJWT) -> None: example_payload = {"hello": "world"} example_secret = "secret" From 1eb72b8553b787957b431ee7336631e6956538d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:11:13 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_api_jwt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py index c826a41e..4aeca725 100644 --- a/tests/test_api_jwt.py +++ b/tests/test_api_jwt.py @@ -59,9 +59,7 @@ def test_encode_with_jwk_uses_key_algorithm(self, jwt: PyJWT) -> None: payload = {"hello": "world"} # Should use HS384 from the key, not default to HS256 token = jwt.encode(payload, jwk) - header = jwt.decode_complete( - token, jwk, algorithms=["HS384"] - )["header"] + header = jwt.decode_complete(token, jwk, algorithms=["HS384"])["header"] assert header["alg"] == "HS384" def test_decodes_valid_jwt(self, jwt: PyJWT) -> None: From 81b81657f7fdd88f65644649f1a8b5559113b305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 12 Mar 2026 13:04:55 -0400 Subject: [PATCH 3/3] update changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc6ad4e7..3a580064 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,7 @@ Fixed - Close ``HTTPError`` response to prevent ``ResourceWarning`` on Python 3.14 by @veeceey in `#1133 `__ - Do not keep ``algorithms`` dict in PyJWK instances by @akx in `#1143 `__ - Validate the crit (Critical) Header Parameter defined in RFC 7515 ยง4.1.11. by @dmbs335 in `GHSA-752w-5fwx-jx9f `__ +- Use PyJWK algorithm when encoding without explicit algorithm in `#1148 `__ Added ~~~~~