pyjwt: FTBFS building against python 3.15

Maximiliano Curia <[email protected]>
Newsgroups gmane.linux.debian.devel.python
Message-ID <[email protected]>
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 /\/\ /\ >< `/
fix-base64url-decode-strict.patch (text/x-diff, 6.5 KB)
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.