Package: src:pyjwt Version: 2.13.0-1 User: [email protected] Usertags: python3.15 Tags: patch
Hi! While rebuilding the python related packages against the python version 3.15rc1 we found that python-botocore fails to build from source[1]. There is an open pull request upstream to fix the problems in the build[2], we backported that fix to allow packages in our sandbox that build against pyjwt to be able to build. Please consider including this patch in your next upload. Happy hacking, [1]: https://debusine.debian.net/debian/r-python-python3.15/work-request/1072657/ [2]: https://github.com/jpadilla/pyjwt/pull/1181 -- "Can you imagine what I would do if I could do all I can?" -- Sun Tzu Saludos /\/\ /\ >< `/
commit e58da927aa78125ec53a66dec1755d4b561cd7c0 Author: arpitjain099 <[email protected]> Date: Thu Jun 4 07:36:22 2026 +0900 Normalize base64 alphabet in base64url_decode to avoid Py3.15 FutureWarning base64url_decode passed input straight to base64.urlsafe_b64decode after padding. When the input carried standard-alphabet characters (+ or /), Python 3.15 emits FutureWarning about invalid characters in URL-safe Base64 data, and CPython has announced it will eventually discard those characters (silently corrupting the decoded bytes). Translate + and / to - and _ before decoding so valid URL-safe input never warns and historical standard-alphabet input keeps decoding to the same bytes deterministically. The test_compressed_jwt fixture also carried a standard-Base64 payload segment (a literal /) rather than the URL-safe form an RFC 7515 JWT requires; rebuild it (and its HMAC signature) on the URL-safe alphabet. Fixes #1167 Signed-off-by: arpitjain099 <[email protected]> diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d52f953..f383bdb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,17 @@ This project adheres to `Semantic Versioning <https://semver.org/>`__. `Unreleased <https://github.com/jpadilla/pyjwt/compare/2.13.0...HEAD>`__ ------------------------------------------------------------------------ +Fixed +~~~~~ + +- Normalise the standard Base64 alphabet (``+`` / ``/``) to the URL-safe + alphabet in ``base64url_decode`` so valid URL-safe input no longer trips + the Python 3.15 ``FutureWarning: invalid character '/' in URL-safe Base64 + data`` and standard-alphabet input keeps decoding deterministically once + CPython starts discarding those characters. Also corrects the + ``test_compressed_jwt`` fixture, which carried a standard-Base64 payload + segment instead of a URL-safe one (`#1167 <https://github.com/jpadilla/pyjwt/issues/1167>`__). + `v2.13.0 <https://github.com/jpadilla/pyjwt/compare/2.12.1...2.13.0>`__ ----------------------------------------------------------------------- diff --git a/jwt/utils.py b/jwt/utils.py index 56e89bb..dcb48e6 100644 --- a/jwt/utils.py +++ b/jwt/utils.py @@ -22,8 +22,18 @@ def force_bytes(value: Union[bytes, str]) -> bytes: raise TypeError("Expected a string value") +# Translate the standard Base64 alphabet to the URL-safe alphabet so that +# callers passing standard-alphabet data (``+`` / ``/``) are normalised before +# the decode. From Python 3.15 ``base64.urlsafe_b64decode`` emits a +# ``FutureWarning`` for ``+`` / ``/`` and has announced it will eventually +# discard those characters, which would silently corrupt the decoded bytes. +# Normalising up front keeps valid URL-safe input warning-free and keeps the +# historical lenient handling of standard-alphabet input deterministic. +_STD_TO_URLSAFE = bytes.maketrans(b"+/", b"-_") + + def base64url_decode(input: Union[bytes, str]) -> bytes: - input_bytes = force_bytes(input) + input_bytes = force_bytes(input).translate(_STD_TO_URLSAFE) rem = len(input_bytes) % 4 diff --git a/tests/test_compressed_jwt.py b/tests/test_compressed_jwt.py index 1968cb4..d80f49e 100644 --- a/tests/test_compressed_jwt.py +++ b/tests/test_compressed_jwt.py @@ -19,11 +19,11 @@ def test_decodes_complete_valid_jwt_with_compressed_payload() -> None: example_payload = {"hello": "world"} example_secret = "secret" # payload made with the pako (https://nodeca.github.io/pako/) library in Javascript: - # Buffer.from(pako.deflateRaw('{"hello": "world"}')).toString('base64') + # Buffer.from(pako.deflateRaw('{"hello": "world"}')).toString('base64url') example_jwt = ( b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" - b".q1bKSM3JyVeyUlAqzy/KSVGqBQA=" - b".08wHYeuh1rJXmcBcMrz6NxmbxAnCQp2rGTKfRNIkxiw=" + b".q1bKSM3JyVeyUlAqzy_KSVGqBQA" + b".AAn1elCJC5MQCYFwTwa2tjjtyLgqLUVU-Y1vFBsU8jo" ) decoded = CompressedPyJWT().decode_complete( example_jwt, example_secret, algorithms=["HS256"] @@ -33,7 +33,7 @@ def test_decodes_complete_valid_jwt_with_compressed_payload() -> None: "header": {"alg": "HS256", "typ": "JWT"}, "payload": example_payload, "signature": ( - b"\xd3\xcc\x07a\xeb\xa1\xd6\xb2W\x99\xc0\\2\xbc\xfa7" - b"\x19\x9b\xc4\t\xc2B\x9d\xab\x192\x9fD\xd2$\xc6," + b"\x00\t\xf5zP\x89\x0b\x93\x10\t\x81pO\x06\xb6\xb6" + b"8\xed\xc8\xb8*-ET\xf9\x8do\x14\x1b\x14\xf2:" ), } diff --git a/tests/test_utils.py b/tests/test_utils.py index 2ad2314..7d18509 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,10 +1,18 @@ +import base64 +import warnings from contextlib import nullcontext import pytest from contextlib import AbstractContextManager -from jwt.utils import force_bytes, from_base64url_uint, is_ssh_key, to_base64url_uint +from jwt.utils import ( + base64url_decode, + force_bytes, + from_base64url_uint, + is_ssh_key, + to_base64url_uint, +) @pytest.mark.parametrize( @@ -41,6 +49,34 @@ def test_from_base64url_uint(inputval: bytes, expected: int) -> None: assert actual == expected +def test_base64url_decode_handles_standard_alphabet() -> None: + # The same bytes encoded with the standard ("+/") and the URL-safe ("-_") + # alphabets must decode to the same value. ``base64url_decode`` normalises + # the standard alphabet so historical callers keep working. + raw = bytes(range(256)) + standard = base64.b64encode(raw) + urlsafe = base64.urlsafe_b64encode(raw).rstrip(b"=") + + assert b"+" in standard or b"/" in standard + assert base64url_decode(standard) == raw + assert base64url_decode(urlsafe) == raw + + +def test_base64url_decode_does_not_warn_on_urlsafe_input() -> None: + # Valid URL-safe input must never trigger the Python 3.15+ FutureWarning + # about "+"/"/" in URL-safe Base64 data (jpadilla/pyjwt#1167). + raw = bytes(range(256)) + standard = base64.b64encode(raw) + urlsafe = base64.urlsafe_b64encode(raw).rstrip(b"=") + + with warnings.catch_warnings(): + warnings.simplefilter("error", FutureWarning) + assert base64url_decode(urlsafe) == raw + # A standard-alphabet input is normalised first, so it must also be + # decoded without emitting the FutureWarning. + assert base64url_decode(standard) == raw + + def test_force_bytes_raises_error_on_invalid_object() -> None: with pytest.raises(TypeError): force_bytes({}) # type: ignore[arg-type]

