From 0c87c8c8b1a74cac99ad8115f3050efcb7fbed35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@webapplicate.com>
Date: Tue, 8 Sep 2026 15:10:07 -0400
Subject: [PATCH] Merge commit from fork
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: José Padilla <jpadilla@users.noreply.github.com>
---
 jwt/api_jwt.py        | 20 +++++++++-------
 tests/test_api_jwt.py | 56 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 67 insertions(+), 9 deletions(-)

Index: pyjwt-2.12.1/jwt/api_jwt.py
===================================================================
--- pyjwt-2.12.1.orig/jwt/api_jwt.py
+++ pyjwt-2.12.1/jwt/api_jwt.py
@@ -76,16 +76,18 @@ class PyJWT:
         if options is None:
             return self.options
 
+        merged_options = cast("Options", dict(options))
+
         # (defensive) set defaults for verify_x to False if verify_signature is False
-        if not options.get("verify_signature", True):
-            options["verify_exp"] = options.get("verify_exp", False)
-            options["verify_nbf"] = options.get("verify_nbf", False)
-            options["verify_iat"] = options.get("verify_iat", False)
-            options["verify_aud"] = options.get("verify_aud", False)
-            options["verify_iss"] = options.get("verify_iss", False)
-            options["verify_sub"] = options.get("verify_sub", False)
-            options["verify_jti"] = options.get("verify_jti", False)
-        return {**self.options, **options}
+        if not merged_options.get("verify_signature", True):
+            merged_options["verify_exp"] = merged_options.get("verify_exp", False)
+            merged_options["verify_nbf"] = merged_options.get("verify_nbf", False)
+            merged_options["verify_iat"] = merged_options.get("verify_iat", False)
+            merged_options["verify_aud"] = merged_options.get("verify_aud", False)
+            merged_options["verify_iss"] = merged_options.get("verify_iss", False)
+            merged_options["verify_sub"] = merged_options.get("verify_sub", False)
+            merged_options["verify_jti"] = merged_options.get("verify_jti", False)
+        return {**self.options, **merged_options}
 
     def encode(
         self,
Index: pyjwt-2.12.1/tests/test_api_jwt.py
===================================================================
--- pyjwt-2.12.1.orig/tests/test_api_jwt.py
+++ pyjwt-2.12.1/tests/test_api_jwt.py
@@ -1,6 +1,7 @@
 import json
 import time
 from calendar import timegm
+from collections.abc import MutableMapping
 from datetime import datetime, timedelta, timezone
 from decimal import Decimal
 
@@ -39,6 +40,61 @@ def payload() -> dict[str, object]:
 
 
 class TestJWT:
+    def test_decode_does_not_mutate_options_when_signature_verification_is_disabled(
+        self, jwt: PyJWT
+    ) -> None:
+        options: Options = {"verify_signature": False}
+        token = jwt.encode({"claim": "value"}, "a" * 32, algorithm="HS256")
+        original_options = options.copy()
+
+        jwt.decode(token, options=options)
+
+        assert options == original_options
+
+    def test_reused_options_do_not_disable_claim_verification(self, jwt: PyJWT) -> None:
+        options: Options = {"verify_signature": False}
+        token = jwt.encode({"exp": utc_timestamp() - 1}, "a" * 32, algorithm="HS256")
+
+        jwt.decode(token, options=options)
+        options["verify_signature"] = True
+
+        with pytest.raises(ExpiredSignatureError):
+            jwt.decode(token, "a" * 32, algorithms=["HS256"], options=options)
+
+    def test_decode_complete_preserves_mutable_mapping_input(self, jwt: PyJWT) -> None:
+        class MappingWithoutCopy(MutableMapping[str, object]):
+            def __init__(self, values: dict[str, object]) -> None:
+                self.values = values
+
+            def __getitem__(self, key: str) -> object:
+                return self.values[key]
+
+            def __setitem__(self, key: str, value: object) -> None:
+                self.values[key] = value
+
+            def __delitem__(self, key: str) -> None:
+                del self.values[key]
+
+            def __iter__(self):
+                return iter(self.values)
+
+            def __len__(self) -> int:
+                return len(self.values)
+
+        options = MappingWithoutCopy({"verify_signature": False})
+        token = jwt.encode({"claim": "value"}, "a" * 32, algorithm="HS256")
+
+        jwt.decode_complete(token, options=options)  # type: ignore[arg-type]
+
+        assert dict(options) == {"verify_signature": False}
+
+    def test_constructor_does_not_mutate_options_input(self, jwt: PyJWT) -> None:
+        options: Options = {"verify_signature": False}
+
+        PyJWT(options=options)
+
+        assert options == {"verify_signature": False}
+
     def test_jwt_with_options(self) -> None:
         jwt = PyJWT(options={"verify_signature": False})
         assert jwt.options["verify_signature"] is False
