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]

Reply via email to