diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fbfa6c84..3f0941af 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,8 +7,39 @@ This project adheres to `Semantic Versioning `__. `Unreleased `__ ------------------------------------------------------------------------ +Security +~~~~~~~~ +- **[CVE-2025-45768]** Added minimum key length validation for HMAC and RSA algorithms to prevent weak encryption by @amanjolhe in `#1085 `__ +- HMAC algorithms now enforce minimum key lengths: HS256 (32 bytes), HS384 (48 bytes), HS512 (64 bytes) +- RSA algorithms now enforce minimum key length of 2048 bits +- Added configurable enforcement via ``set_min_key_length_enforcement()`` and ``get_min_key_length_enforcement()`` +- Validation applies to all key input methods: direct bytes, PEM format, and JWK format +- Complies with security standards: RFC 7518, NIST SP800-117, and RFC 2437 + +Added +~~~~~ +- ``set_min_key_length_enforcement(enforce: bool)`` - Configure key length validation behavior by @amanjolhe in `#1085 `__ +- ``get_min_key_length_enforcement() -> bool`` - Get current validation behavior by @amanjolhe in `#1085 `__ +- Security warnings for weak keys when enforcement is disabled (deprecated mode) + +Changed +~~~~~~~ +- Default behavior now enforces minimum key lengths (can be disabled temporarily) +- Weak keys will raise ``InvalidKeyError`` by default instead of being silently accepted + +Deprecated +~~~~~~~~~~ +- Disabling key length enforcement is deprecated and will be removed in PyJWT 3.0 +- Direct access to ``ENFORCE_MIN_KEY_LENGTH`` variable is deprecated + Fixed ~~~~~ +- **Security**: Implement minimum key length validation for HMAC and RSA algorithms to address CVE-2025-45768 by @adeshjolhe in `#1085 `__ + + - HMAC algorithms now require keys of at least 32 bytes (HS256), 48 bytes (HS384), and 64 bytes (HS512) + - RSA algorithms now require keys of at least 2048 bits as per RFC 7518 and NIST SP800-117 recommendations + - Added comprehensive validation in both prepare_key() and from_jwk() methods + - Updated documentation examples to use secure key lengths - Validate key against allowed types for Algorithm family in `#964 `__ - Add iterator for JWKSet in `#1041 `__ - Validate `iss` claim is a string during encoding and decoding by @pachewise in `#1040 `__ diff --git a/docs/index.rst b/docs/index.rst index e4428d17..e2f7900a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,8 +30,10 @@ Example Usage .. doctest:: >>> import jwt - >>> encoded_jwt = jwt.encode({"some": "payload"}, "secret", algorithm="HS256") - >>> jwt.decode(encoded_jwt, "secret", algorithms=["HS256"]) + >>> encoded_jwt = jwt.encode( + ... {"some": "payload"}, "your-256-bit-secret-key-here-32chars", algorithm="HS256" + ... ) + >>> jwt.decode(encoded_jwt, "your-256-bit-secret-key-here-32chars", algorithms=["HS256"]) {'some': 'payload'} See :doc:`Usage Examples ` for more examples. diff --git a/docs/usage.rst b/docs/usage.rst index e3cfc74b..82e1ff29 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,7 +7,7 @@ Encoding & Decoding Tokens with HS256 .. code-block:: pycon >>> import jwt - >>> key = "secret" + >>> key = "your-256-bit-secret-key-here-32chars" >>> encoded = jwt.encode({"some": "payload"}, key, algorithm="HS256") >>> jwt.decode(encoded, key, algorithms="HS256") {'some': 'payload'} @@ -95,7 +95,7 @@ Specifying Additional Headers >>> jwt.encode( ... {"some": "payload"}, - ... "secret", + ... "your-256-bit-secret-key-here-32chars", ... algorithm="HS256", ... headers={"kid": "230498151c214b788dd97f22b85410a5"}, ... ) @@ -108,7 +108,7 @@ By default the ``typ`` is attaching to the headers. In case when you don't need >>> jwt.encode( ... {"some": "payload"}, - ... "secret", + ... "your-256-bit-secret-key-here-32chars", ... algorithm="HS256", ... headers={"typ": None}, ... ) @@ -143,7 +143,7 @@ key in the header. >>> encoded = jwt.encode( ... {"some": "payload"}, - ... "secret", + ... "your-256-bit-secret-key-here-32chars", ... algorithm="HS256", ... headers={"kid": "230498151c214b788dd97f22b85410a5"}, ... ) @@ -179,8 +179,10 @@ datetime, which will be converted into an int. For example: .. code-block:: pycon >>> from datetime import datetime, timezone - >>> token = jwt.encode({"exp": 1371720939}, "secret") - >>> token = jwt.encode({"exp": datetime.now(tz=timezone.utc)}, "secret") + >>> token = jwt.encode({"exp": 1371720939}, "your-256-bit-secret-key-here-32chars") + >>> token = jwt.encode( + ... {"exp": datetime.now(tz=timezone.utc)}, "your-256-bit-secret-key-here-32chars" + ... ) Expiration time is automatically verified in `jwt.decode()` and raises `jwt.ExpiredSignatureError` if the expiration time is in the past: @@ -188,7 +190,7 @@ Expiration time is automatically verified in `jwt.decode()` and raises .. code-block:: pycon >>> try: - ... jwt.decode(token, "secret", algorithms=["HS256"]) + ... jwt.decode(token, "your-256-bit-secret-key-here-32chars", algorithms=["HS256"]) ... except jwt.ExpiredSignatureError: ... print("expired") ... @@ -213,11 +215,13 @@ you can set a leeway of 10 seconds in order to have some margin: >>> payload = { ... "exp": datetime.datetime.now(tz=timezone.utc) + datetime.timedelta(seconds=1) ... } - >>> token = jwt.encode(payload, "secret") + >>> token = jwt.encode(payload, "your-256-bit-secret-key-here-32chars") >>> time.sleep(2) >>> # JWT payload is now expired >>> # But with some leeway, it will still validate - >>> decoded = jwt.decode(token, "secret", leeway=5, algorithms=["HS256"]) + >>> decoded = jwt.decode( + ... token, "your-256-bit-secret-key-here-32chars", leeway=5, algorithms=["HS256"] + ... ) Instead of specifying the leeway as a number of seconds, a `datetime.timedelta` instance can be used. The last line in the example above is equivalent to: @@ -225,7 +229,10 @@ instance can be used. The last line in the example above is equivalent to: .. code-block:: pycon >>> decoded = jwt.decode( - ... token, "secret", leeway=datetime.timedelta(seconds=10), algorithms=["HS256"] + ... token, + ... "your-256-bit-secret-key-here-32chars", + ... leeway=datetime.timedelta(seconds=10), + ... algorithms=["HS256"], ... ) Not Before Time Claim (nbf) @@ -243,8 +250,11 @@ The `nbf` claim works similarly to the `exp` claim above. .. code-block:: pycon - >>> token = jwt.encode({"nbf": 1371720939}, "secret") - >>> token = jwt.encode({"nbf": datetime.datetime.now(tz=timezone.utc)}, "secret") + >>> token = jwt.encode({"nbf": 1371720939}, "your-256-bit-secret-key-here-32chars") + >>> token = jwt.encode( + ... {"nbf": datetime.datetime.now(tz=timezone.utc)}, + ... "your-256-bit-secret-key-here-32chars", + ... ) The `nbf` claim also supports the leeway feature similar to the `exp` claim. This allows you to validate a “not before” time that is slightly in the future. Using @@ -258,10 +268,12 @@ synchronization between the token issuer and the validator is imprecise. >>> payload = { ... "nbf": datetime.datetime.now(tz=timezone.utc) - datetime.timedelta(seconds=3) ... } - >>> token = jwt.encode(payload, "secret") + >>> token = jwt.encode(payload, "your-256-bit-secret-key-here-32chars") >>> # JWT payload is not valid yet >>> # But with some leeway, it will still validate - >>> decoded = jwt.decode(token, "secret", leeway=5, algorithms=["HS256"]) + >>> decoded = jwt.decode( + ... token, "your-256-bit-secret-key-here-32chars", leeway=5, algorithms=["HS256"] + ... ) Issuer Claim (iss) @@ -275,9 +287,14 @@ Issuer Claim (iss) .. code-block:: pycon >>> payload = {"some": "payload", "iss": "urn:foo"} - >>> token = jwt.encode(payload, "secret") + >>> token = jwt.encode(payload, "your-256-bit-secret-key-here-32chars") >>> try: - ... jwt.decode(token, "secret", issuer="urn:invalid", algorithms=["HS256"]) + ... jwt.decode( + ... token, + ... "your-256-bit-secret-key-here-32chars", + ... issuer="urn:invalid", + ... algorithms=["HS256"], + ... ) ... except jwt.InvalidIssuerError: ... print("invalid issuer") ... @@ -301,9 +318,19 @@ sensitive strings, each containing a StringOrURI value. .. code-block:: pycon >>> payload = {"some": "payload", "aud": ["urn:foo", "urn:bar"]} - >>> token = jwt.encode(payload, "secret") - >>> decoded = jwt.decode(token, "secret", audience="urn:foo", algorithms=["HS256"]) - >>> decoded = jwt.decode(token, "secret", audience="urn:bar", algorithms=["HS256"]) + >>> token = jwt.encode(payload, "your-256-bit-secret-key-here-32chars") + >>> decoded = jwt.decode( + ... token, + ... "your-256-bit-secret-key-here-32chars", + ... audience="urn:foo", + ... algorithms=["HS256"], + ... ) + >>> decoded = jwt.decode( + ... token, + ... "your-256-bit-secret-key-here-32chars", + ... audience="urn:bar", + ... algorithms=["HS256"], + ... ) In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. @@ -311,8 +338,13 @@ a single case-sensitive string containing a StringOrURI value. .. code-block:: pycon >>> payload = {"some": "payload", "aud": "urn:foo"} - >>> token = jwt.encode(payload, "secret") - >>> decoded = jwt.decode(token, "secret", audience="urn:foo", algorithms=["HS256"]) + >>> token = jwt.encode(payload, "your-256-bit-secret-key-here-32chars") + >>> decoded = jwt.decode( + ... token, + ... "your-256-bit-secret-key-here-32chars", + ... audience="urn:foo", + ... algorithms=["HS256"], + ... ) If multiple audiences are accepted, the ``audience`` parameter for ``jwt.decode`` can also be an iterable @@ -320,12 +352,20 @@ If multiple audiences are accepted, the ``audience`` parameter for .. code-block:: pycon >>> payload = {"some": "payload", "aud": "urn:foo"} - >>> token = jwt.encode(payload, "secret") + >>> token = jwt.encode(payload, "your-256-bit-secret-key-here-32chars") >>> decoded = jwt.decode( - ... token, "secret", audience=["urn:foo", "urn:bar"], algorithms=["HS256"] + ... token, + ... "your-256-bit-secret-key-here-32chars", + ... audience=["urn:foo", "urn:bar"], + ... algorithms=["HS256"], ... ) >>> try: - ... jwt.decode(token, "secret", audience=["urn:invalid"], algorithms=["HS256"]) + ... jwt.decode( + ... token, + ... "your-256-bit-secret-key-here-32chars", + ... audience=["urn:invalid"], + ... algorithms=["HS256"], + ... ) ... except jwt.InvalidAudienceError: ... print("invalid audience") ... @@ -347,8 +387,11 @@ Issued At Claim (iat) .. code-block:: pycon - >>> token = jwt.encode({"iat": 1371720939}, "secret") - >>> token = jwt.encode({"iat": datetime.datetime.now(tz=timezone.utc)}, "secret") + >>> token = jwt.encode({"iat": 1371720939}, "your-256-bit-secret-key-here-32chars") + >>> token = jwt.encode( + ... {"iat": datetime.datetime.now(tz=timezone.utc)}, + ... "your-256-bit-secret-key-here-32chars", + ... ) Requiring Presence of Claims ---------------------------- @@ -357,11 +400,13 @@ If you wish to require one or more claims to be present in the claimset, you can .. code-block:: pycon - >>> token = jwt.encode({"sub": "1234567890", "iat": 1371720939}, "secret") + >>> token = jwt.encode( + ... {"sub": "1234567890", "iat": 1371720939}, "your-256-bit-secret-key-here-32chars" + ... ) >>> try: ... jwt.decode( ... token, - ... "secret", + ... "your-256-bit-secret-key-here-32chars", ... options={"require": ["exp", "iss", "sub"]}, ... algorithms=["HS256"], ... ) @@ -459,3 +504,92 @@ is not built into pyjwt. digest = alg_obj.compute_hash_digest(access_token) at_hash = base64.urlsafe_b64encode(digest[: (len(digest) // 2)]).rstrip("=") assert at_hash == payload["at_hash"] + + +Security Considerations +======================= + +Key Length Validation +--------------------- + +Starting with PyJWT 2.11.0, the library enforces minimum key lengths for cryptographic security: + +- **HMAC algorithms**: HS256 (32 bytes), HS384 (48 bytes), HS512 (64 bytes) +- **RSA algorithms**: 2048 bits minimum + +This validation helps prevent weak key attacks and ensures compliance with security standards (RFC 7518, NIST SP800-117, RFC 2437). + +.. code-block:: python + + import jwt + + # These will work (secure keys) + strong_hmac_key = b"your-32-byte-secret-key-here!" # 32 bytes for HS256 + token = jwt.encode({"user": "john"}, strong_hmac_key, algorithm="HS256") + + # This will raise InvalidKeyError (weak key) + weak_key = b"short" # Only 5 bytes + token = jwt.encode( + {"user": "john"}, weak_key, algorithm="HS256" + ) # Raises InvalidKeyError + +Configuring Key Length Validation +--------------------------------- + +For migration purposes, you can temporarily disable strict enforcement: + +.. code-block:: python + + import jwt + import warnings + + # Check current setting + enforcement = jwt.get_min_key_length_enforcement() + print(f"Enforcement enabled: {enforcement}") # True by default + + # Temporary warning mode (deprecated - for migration only) + jwt.set_min_key_length_enforcement(False) + + with warnings.catch_warnings(): + warnings.simplefilter("always") # Show security warnings + token = jwt.encode({"user": "john"}, weak_key, algorithm="HS256") # Issues warning + + # Re-enable enforcement (recommended) + jwt.set_min_key_length_enforcement(True) + +.. warning:: + Disabling key length enforcement is deprecated and will be removed in PyJWT 3.0. + Please migrate to using cryptographically secure key lengths. + +Generating Secure Keys +--------------------- + +For HMAC algorithms, use the ``secrets`` module to generate cryptographically secure keys: + +.. code-block:: python + + import secrets + + # Generate secure HMAC keys + hs256_key = secrets.token_bytes(32) # 32 bytes = 256 bits + hs384_key = secrets.token_bytes(48) # 48 bytes = 384 bits + hs512_key = secrets.token_bytes(64) # 64 bytes = 512 bits + +For RSA algorithms, use the ``cryptography`` library to generate keys with appropriate bit lengths: + +.. code-block:: python + + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives import serialization + + # Generate secure RSA key (2048 bits minimum) + private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048 # or 3072, 4096 for higher security + ) + + # Serialize for use with PyJWT + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) diff --git a/jwt/__init__.py b/jwt/__init__.py index 457a4e35..3194523d 100644 --- a/jwt/__init__.py +++ b/jwt/__init__.py @@ -1,3 +1,7 @@ +from .algorithms import ( + get_min_key_length_enforcement, + set_min_key_length_enforcement, +) from .api_jwk import PyJWK, PyJWKSet from .api_jws import ( PyJWS, @@ -55,6 +59,8 @@ "register_algorithm", "unregister_algorithm", "get_algorithm_by_name", + "get_min_key_length_enforcement", + "set_min_key_length_enforcement", # Exceptions "DecodeError", "ExpiredSignatureError", diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 47d77df0..65612386 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -4,6 +4,7 @@ import hmac import json import os +import warnings from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, ClassVar, Literal, NoReturn, cast, overload @@ -124,6 +125,64 @@ has_crypto = False +# Minimum key length validation configuration +_enforce_min_key_length = True # Private variable +_deprecation_warning_issued = False # Track if deprecation warning was shown + + +def set_min_key_length_enforcement(enforce: bool) -> None: + """ + Configure minimum key length validation behavior. + + Args: + enforce (bool): + - True (default): Raises InvalidKeyError for keys below minimum length + - False: Emits a security warning but allows the operation to continue + + Note: + The ability to disable enforcement is deprecated and will be removed + in PyJWT 3.0. After that version, minimum key length validation will + always be enforced. + + Example: + # Temporary warning mode (deprecated - use only for migration) + jwt.algorithms.set_min_key_length_enforcement(False) + + # Recommended: Use strong keys and keep enforcement enabled (default) + jwt.algorithms.set_min_key_length_enforcement(True) + """ + global _enforce_min_key_length, _deprecation_warning_issued + + _enforce_min_key_length = enforce + + # Issue deprecation warning when disabling enforcement + if not enforce and not _deprecation_warning_issued: + warnings.warn( + "Disabling minimum key length enforcement is deprecated and will be " + "removed in PyJWT 3.0. Please migrate to using cryptographically " + "secure key lengths. See https://pyjwt.readthedocs.io/en/latest/usage.html#security", + DeprecationWarning, + stacklevel=2, + ) + _deprecation_warning_issued = True + + +def get_min_key_length_enforcement() -> bool: + """ + Get the current minimum key length validation behavior. + + Returns: + bool: True if enforcement is enabled, False if only warnings are issued + """ + return _enforce_min_key_length + + +# Backward compatibility - will be removed in PyJWT 3.0 +# Note: Direct access to this variable is deprecated +# Use set_min_key_length_enforcement() and get_min_key_length_enforcement() instead +ENFORCE_MIN_KEY_LENGTH = _enforce_min_key_length + + requires_cryptography = { "RS256", "RS384", @@ -316,6 +375,18 @@ class HMACAlgorithm(Algorithm): def __init__(self, hash_alg: HashlibHash) -> None: self.hash_alg = hash_alg + def _get_min_key_length(self) -> int: + """Get minimum key length in bytes based on hash algorithm.""" + if self.hash_alg == hashlib.sha256: + return 32 # 256 bits for HS256 + elif self.hash_alg == hashlib.sha384: + return 48 # 384 bits for HS384 + elif self.hash_alg == hashlib.sha512: + return 64 # 512 bits for HS512 + else: + # For any other hash algorithm, require at least 32 bytes (256 bits) + return 32 + def prepare_key(self, key: str | bytes) -> bytes: key_bytes = force_bytes(key) @@ -325,6 +396,34 @@ def prepare_key(self, key: str | bytes) -> bytes: " should not be used as an HMAC secret." ) + # Enforce minimum key lengths per RFC 7518 and NIST guidelines + min_key_length = self._get_min_key_length() + if len(key_bytes) < min_key_length: + # Get algorithm name for error message + alg_name = "HMAC" + if self.hash_alg == hashlib.sha256: + alg_name = "HS256" + elif self.hash_alg == hashlib.sha384: + alg_name = "HS384" + elif self.hash_alg == hashlib.sha512: + alg_name = "HS512" + + message = ( + f"HMAC key must be at least {min_key_length * 8} bits " + f"({min_key_length} bytes) for {alg_name} algorithm. " + f"Key provided is {len(key_bytes) * 8} bits ({len(key_bytes)} bytes)." + ) + + if get_min_key_length_enforcement(): + raise InvalidKeyError(message) + else: + warnings.warn( + f"Security Warning: {message} " + "This will be enforced in a future version.", + UserWarning, + stacklevel=2, + ) + return key_bytes @overload @@ -366,7 +465,22 @@ def from_jwk(jwk: str | JWKDict) -> bytes: if obj.get("kty") != "oct": raise InvalidKeyError("Not an HMAC key") - return base64url_decode(obj["k"]) + key_bytes = base64url_decode(obj["k"]) + + # Validate key length - use a conservative minimum of 32 bytes (256 bits) + min_key_length = 32 # 256 bits minimum + if len(key_bytes) < min_key_length: + message = ( + f"HMAC key must be at least {min_key_length * 8} bits " + f"({min_key_length} bytes). Key provided is {len(key_bytes) * 8} " + f"bits ({len(key_bytes)} bytes)." + ) + if get_min_key_length_enforcement(): + raise InvalidKeyError(message) + else: + warnings.warn(message, UserWarning, stacklevel=3) + + return key_bytes def sign(self, msg: bytes, key: bytes) -> bytes: return hmac.new(key, msg, self.hash_alg).digest() @@ -392,8 +506,40 @@ class RSAAlgorithm(Algorithm): def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: self.hash_alg = hash_alg + def _validate_rsa_key_size(self, key: AllowedRSAKeys) -> None: + """Validate RSA key size meets minimum security requirements.""" + key_size = key.key_size + min_key_size = 2048 # Minimum 2048 bits per RFC 7518 and NIST SP800-117 + + if key_size < min_key_size: + message = ( + f"RSA key must be at least {min_key_size} bits. " + f"Key provided is {key_size} bits." + ) + if get_min_key_length_enforcement(): + raise InvalidKeyError(message) + else: + warnings.warn(message, UserWarning, stacklevel=3) + + @staticmethod + def _validate_rsa_key_size_static(key: AllowedRSAKeys) -> None: + """Static version of RSA key size validation for use in static methods.""" + key_size = key.key_size + min_key_size = 2048 # Minimum 2048 bits per RFC 7518 and NIST SP800-117 + + if key_size < min_key_size: + message = ( + f"RSA key must be at least {min_key_size} bits. " + f"Key provided is {key_size} bits." + ) + if get_min_key_length_enforcement(): + raise InvalidKeyError(message) + else: + warnings.warn(message, UserWarning, stacklevel=3) + def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: if isinstance(key, self._crypto_key_types): + self._validate_rsa_key_size(key) return key if not isinstance(key, (bytes, str)): @@ -405,18 +551,24 @@ def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: if key_bytes.startswith(b"ssh-rsa"): public_key: PublicKeyTypes = load_ssh_public_key(key_bytes) self.check_crypto_key_type(public_key) - return cast(RSAPublicKey, public_key) + rsa_public_key = cast(RSAPublicKey, public_key) + self._validate_rsa_key_size(rsa_public_key) + return rsa_public_key else: private_key: PrivateKeyTypes = load_pem_private_key( key_bytes, password=None ) self.check_crypto_key_type(private_key) - return cast(RSAPrivateKey, private_key) + rsa_private_key = cast(RSAPrivateKey, private_key) + self._validate_rsa_key_size(rsa_private_key) + return rsa_private_key except ValueError: try: public_key = load_pem_public_key(key_bytes) self.check_crypto_key_type(public_key) - return cast(RSAPublicKey, public_key) + rsa_public_key = cast(RSAPublicKey, public_key) + self._validate_rsa_key_size(rsa_public_key) + return rsa_public_key except (ValueError, UnsupportedAlgorithm): raise InvalidKeyError( "Could not parse the provided public key." @@ -519,6 +671,9 @@ def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: iqmp=from_base64url_uint(obj["qi"]), public_numbers=public_numbers, ) + private_key = numbers.private_key() + RSAAlgorithm._validate_rsa_key_size_static(private_key) + return private_key else: d = from_base64url_uint(obj["d"]) p, q = rsa_recover_prime_factors( @@ -535,13 +690,17 @@ def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: public_numbers=public_numbers, ) - return numbers.private_key() + private_key = numbers.private_key() + RSAAlgorithm._validate_rsa_key_size_static(private_key) + return private_key elif "n" in obj and "e" in obj: # Public key - return RSAPublicNumbers( + public_key = RSAPublicNumbers( from_base64url_uint(obj["e"]), from_base64url_uint(obj["n"]), ).public_key() + RSAAlgorithm._validate_rsa_key_size_static(public_key) + return public_key else: raise InvalidKeyError("Not a public or private key") @@ -793,7 +952,7 @@ def __init__(self, **kwargs: Any) -> None: def prepare_key(self, key: AllowedOKPKeys | str | bytes) -> AllowedOKPKeys: if not isinstance(key, (str, bytes)): self.check_crypto_key_type(key) - return cast("AllowedOKPKeys", key) + return key key_str = key.decode("utf-8") if isinstance(key, bytes) else key key_bytes = key.encode("utf-8") if isinstance(key, str) else key diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 0c061d62..65d05d5d 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -6,12 +6,13 @@ from jwt.algorithms import HMACAlgorithm, NoneAlgorithm, has_crypto from jwt.exceptions import InvalidKeyError -from jwt.utils import base64url_decode +from jwt.utils import base64url_decode, base64url_encode from .keys import load_ec_pub_key_p_521, load_hmac_key, load_rsa_pub_key from .utils import crypto_required, key_path if has_crypto: + from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric.ec import ( EllipticCurvePrivateKey, EllipticCurvePublicKey, @@ -70,7 +71,7 @@ def test_hmac_should_reject_nonstring_key(self): def test_hmac_should_accept_unicode_key(self): algo = HMACAlgorithm(HMACAlgorithm.SHA256) - algo.prepare_key("awesome") + algo.prepare_key("awesome" * 5) # 35 characters > 32 bytes minimum @pytest.mark.parametrize( "key", @@ -101,12 +102,12 @@ def test_hmac_jwk_should_parse_and_verify(self): @pytest.mark.parametrize("as_dict", (False, True)) def test_hmac_to_jwk_returns_correct_values(self, as_dict): algo = HMACAlgorithm(HMACAlgorithm.SHA256) - key: Any = algo.to_jwk("secret", as_dict=as_dict) + key: Any = algo.to_jwk("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", as_dict=as_dict) if not as_dict: key = json.loads(key) - assert key == {"kty": "oct", "k": "c2VjcmV0"} + assert key == {"kty": "oct", "k": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE"} def test_hmac_from_jwk_should_raise_exception_if_not_hmac_key(self): algo = HMACAlgorithm(HMACAlgorithm.SHA256) @@ -122,6 +123,57 @@ def test_hmac_from_jwk_should_raise_exception_if_empty_json(self): with pytest.raises(InvalidKeyError): algo.from_jwk(keyfile.read()) + # CVE-2025-45768: Test minimum key length enforcement + @pytest.mark.parametrize( + "hash_alg,min_length,weak_key", + [ + (HMACAlgorithm.SHA256, 32, b"short"), # 5 bytes, too short for HS256 + (HMACAlgorithm.SHA256, 32, b"a" * 31), # 31 bytes, just under minimum + (HMACAlgorithm.SHA384, 48, b"b" * 47), # 47 bytes, just under minimum + (HMACAlgorithm.SHA512, 64, b"c" * 63), # 63 bytes, just under minimum + ], + ) + def test_hmac_should_reject_weak_keys(self, hash_alg, min_length, weak_key): + """Test that HMAC keys below minimum length are rejected (CVE-2025-45768)""" + algo = HMACAlgorithm(hash_alg) + + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(weak_key) + + error_msg = str(excinfo.value) + assert f"at least {min_length * 8} bits" in error_msg + assert f"Key provided is {len(weak_key) * 8} bits" in error_msg + + @pytest.mark.parametrize( + "hash_alg,adequate_key", + [ + (HMACAlgorithm.SHA256, b"a" * 32), # 32 bytes for HS256 + (HMACAlgorithm.SHA384, b"b" * 48), # 48 bytes for HS384 + (HMACAlgorithm.SHA512, b"c" * 64), # 64 bytes for HS512 + ], + ) + def test_hmac_should_accept_adequate_keys(self, hash_alg, adequate_key): + """Test that HMAC keys at or above minimum length are accepted""" + algo = HMACAlgorithm(hash_alg) + + # Should not raise an exception + prepared_key = algo.prepare_key(adequate_key) + assert prepared_key == adequate_key + + def test_hmac_from_jwk_should_reject_weak_keys(self): + """Test that weak HMAC keys are rejected when loaded from JWK (CVE-2025-45768)""" + algo = HMACAlgorithm(HMACAlgorithm.SHA256) + + # Create a JWK with a weak key (5 bytes) + weak_jwk = {"kty": "oct", "k": "c2hvcnQ"} # base64url("short") - only 5 bytes + + with pytest.raises(InvalidKeyError) as excinfo: + algo.from_jwk(weak_jwk) + + error_msg = str(excinfo.value) + assert "at least 256 bits" in error_msg + assert "40 bits" in error_msg # 5 bytes * 8 = 40 bits + @crypto_required def test_rsa_should_parse_pem_public_key(self): algo = RSAAlgorithm(RSAAlgorithm.SHA256) @@ -173,6 +225,75 @@ def test_rsa_verify_should_return_false_if_signature_invalid(self): result = algo.verify(message, pub_key, sig) assert not result + # CVE-2025-45768: Test RSA minimum key size enforcement + @crypto_required + def test_rsa_should_reject_weak_keys(self): + """Test that RSA keys below 2048 bits are rejected (CVE-2025-45768)""" + from cryptography.hazmat.primitives.asymmetric import rsa + + algo = RSAAlgorithm(RSAAlgorithm.SHA256) + + # Generate a weak 1024-bit RSA key + weak_private_key = rsa.generate_private_key( + public_exponent=65537, key_size=1024 + ) + weak_public_key = weak_private_key.public_key() + + # Test with private key + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(weak_private_key) + + error_msg = str(excinfo.value) + assert "at least 2048 bits" in error_msg + assert "1024 bits" in error_msg + + # Test with public key + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(weak_public_key) + + error_msg = str(excinfo.value) + assert "at least 2048 bits" in error_msg + assert "1024 bits" in error_msg + + @crypto_required + def test_rsa_should_accept_adequate_keys(self): + """Test that RSA keys at or above 2048 bits are accepted""" + from cryptography.hazmat.primitives.asymmetric import rsa + + algo = RSAAlgorithm(RSAAlgorithm.SHA256) + + # Generate a strong 2048-bit RSA key + strong_private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048 + ) + strong_public_key = strong_private_key.public_key() + + # Should not raise exceptions + prepared_private = algo.prepare_key(strong_private_key) + prepared_public = algo.prepare_key(strong_public_key) + + assert prepared_private == strong_private_key + assert prepared_public == strong_public_key + + @crypto_required + def test_rsa_from_jwk_should_reject_weak_keys(self): + """Test that weak RSA keys are rejected when loaded from JWK (CVE-2025-45768)""" + from cryptography.hazmat.primitives.asymmetric import rsa + + # Generate a weak 1024-bit RSA key and convert to JWK + weak_key = rsa.generate_private_key(public_exponent=65537, key_size=1024) + + # Convert to JWK format (this will work since to_jwk doesn't validate) + weak_jwk = RSAAlgorithm.to_jwk(weak_key, as_dict=True) + + # Now try to load it back - should fail + with pytest.raises(InvalidKeyError) as excinfo: + RSAAlgorithm.from_jwk(weak_jwk) + + error_msg = str(excinfo.value) + assert "at least 2048 bits" in error_msg + assert "1024 bits" in error_msg + @crypto_required def test_ec_jwk_public_and_private_keys_should_parse_and_verify(self): tests = { @@ -1162,3 +1283,182 @@ def test_rsa_prepare_key_raises_invalid_key_error_on_invalid_pem(self): # Check that the exception message is correct assert "Could not parse the provided public key." in str(excinfo.value) + + +class TestSecurityValidation: + """Tests for CVE-2025-45768 security validation features.""" + + def test_hmac_get_min_key_length_sha256(self): + """Test minimum key length for SHA256.""" + algo = HMACAlgorithm(HMACAlgorithm.SHA256) + assert algo._get_min_key_length() == 32 + + def test_hmac_get_min_key_length_sha384(self): + """Test minimum key length for SHA384.""" + algo = HMACAlgorithm(HMACAlgorithm.SHA384) + assert algo._get_min_key_length() == 48 + + def test_hmac_get_min_key_length_sha512(self): + """Test minimum key length for SHA512.""" + algo = HMACAlgorithm(HMACAlgorithm.SHA512) + assert algo._get_min_key_length() == 64 + + def test_hmac_get_min_key_length_unknown_algorithm(self): + """Test minimum key length for unknown hash algorithm.""" + # Create an HMAC algorithm with a different hash function + import hashlib + + algo = HMACAlgorithm(hashlib.sha1) # Use SHA1 as "unknown" algorithm + assert algo._get_min_key_length() == 32 # Should default to 32 bytes + + def test_hmac_prepare_key_rejects_short_key_hs256(self): + """Test HS256 rejects keys shorter than 32 bytes.""" + algo = HMACAlgorithm(HMACAlgorithm.SHA256) + short_key = b"short" # Only 5 bytes + + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(short_key) + + assert ( + "HMAC key must be at least 256 bits (32 bytes) for HS256 algorithm" + in str(excinfo.value) + ) + assert "Key provided is 40 bits (5 bytes)" in str(excinfo.value) + + def test_hmac_prepare_key_rejects_short_key_hs384(self): + """Test HS384 rejects keys shorter than 48 bytes.""" + algo = HMACAlgorithm(HMACAlgorithm.SHA384) + short_key = b"a" * 32 # Only 32 bytes, need 48 + + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(short_key) + + assert ( + "HMAC key must be at least 384 bits (48 bytes) for HS384 algorithm" + in str(excinfo.value) + ) + assert "Key provided is 256 bits (32 bytes)" in str(excinfo.value) + + def test_hmac_prepare_key_rejects_short_key_hs512(self): + """Test HS512 rejects keys shorter than 64 bytes.""" + algo = HMACAlgorithm(HMACAlgorithm.SHA512) + short_key = b"a" * 48 # Only 48 bytes, need 64 + + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(short_key) + + assert ( + "HMAC key must be at least 512 bits (64 bytes) for HS512 algorithm" + in str(excinfo.value) + ) + assert "Key provided is 384 bits (48 bytes)" in str(excinfo.value) + + def test_hmac_prepare_key_rejects_short_key_unknown_algorithm(self): + """Test unknown hash algorithm rejects keys shorter than 32 bytes.""" + import hashlib + + algo = HMACAlgorithm(hashlib.sha1) # Unknown algorithm + short_key = b"short" # Only 5 bytes + + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(short_key) + + assert ( + "HMAC key must be at least 256 bits (32 bytes) for HMAC algorithm" + in str(excinfo.value) + ) + + def test_hmac_from_jwk_rejects_short_key(self): + """Test HMAC from_jwk rejects short keys.""" + # Create a JWK with a short key (only 16 bytes = 128 bits) + short_key_b64 = base64url_encode(b"a" * 16) + jwk = {"kty": "oct", "k": short_key_b64} + + with pytest.raises(InvalidKeyError) as excinfo: + HMACAlgorithm.from_jwk(jwk) + + assert "HMAC key must be at least 256 bits (32 bytes)" in str(excinfo.value) + assert "Key provided is 128 bits (16 bytes)" in str(excinfo.value) + + @crypto_required + def test_rsa_validate_key_size_rejects_small_key(self): + """Test RSA validation rejects keys smaller than 2048 bits.""" + algo = RSAAlgorithm(RSAAlgorithm.SHA256) + + # Generate a 1024-bit RSA key (too small) + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=1024, + ) + + with pytest.raises(InvalidKeyError) as excinfo: + algo._validate_rsa_key_size(private_key) + + assert "RSA key must be at least 2048 bits" in str(excinfo.value) + assert "Key provided is 1024 bits" in str(excinfo.value) + + @crypto_required + def test_rsa_validate_key_size_static_rejects_small_key(self): + """Test static RSA validation rejects keys smaller than 2048 bits.""" + # Generate a 1024-bit RSA key (too small) + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=1024, + ) + + with pytest.raises(InvalidKeyError) as excinfo: + RSAAlgorithm._validate_rsa_key_size_static(private_key) + + assert "RSA key must be at least 2048 bits" in str(excinfo.value) + assert "Key provided is 1024 bits" in str(excinfo.value) + + @crypto_required + def test_rsa_prepare_key_validates_existing_key_size(self): + """Test RSA prepare_key validates size of existing key objects.""" + algo = RSAAlgorithm(RSAAlgorithm.SHA256) + + # Generate a 1024-bit RSA key (too small) + small_key = rsa.generate_private_key( + public_exponent=65537, + key_size=1024, + ) + + with pytest.raises(InvalidKeyError) as excinfo: + algo.prepare_key(small_key) + + assert "RSA key must be at least 2048 bits" in str(excinfo.value) + + @crypto_required + def test_rsa_from_jwk_validates_private_key_size(self): + """Test RSA from_jwk validates private key size.""" + # Create a small RSA key for testing + small_key = rsa.generate_private_key( + public_exponent=65537, + key_size=1024, + ) + + # Convert to JWK format using the existing to_jwk method + jwk = RSAAlgorithm.to_jwk(small_key, as_dict=True) + + with pytest.raises(InvalidKeyError) as excinfo: + RSAAlgorithm.from_jwk(jwk) + + assert "RSA key must be at least 2048 bits" in str(excinfo.value) + + @crypto_required + def test_rsa_from_jwk_validates_public_key_size(self): + """Test RSA from_jwk validates public key size.""" + # Create a small RSA key for testing + small_key = rsa.generate_private_key( + public_exponent=65537, + key_size=1024, + ) + + # Convert to public JWK format + public_key = small_key.public_key() + jwk = RSAAlgorithm.to_jwk(public_key, as_dict=True) + + with pytest.raises(InvalidKeyError) as excinfo: + RSAAlgorithm.from_jwk(jwk) + + assert "RSA key must be at least 2048 bits" in str(excinfo.value) diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py index 3efdc0db..e7ac20ab 100644 --- a/tests/test_api_jws.py +++ b/tests/test_api_jws.py @@ -9,6 +9,7 @@ from jwt.exceptions import ( DecodeError, InvalidAlgorithmError, + InvalidKeyError, InvalidSignatureError, InvalidTokenError, ) @@ -77,9 +78,13 @@ def test_override_options(self): assert not jws.options["verify_signature"] def test_non_object_options_dont_persist(self, jws, payload): - token = jws.encode(payload, "secret") + token = jws.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - jws.decode(token, "secret", options={"verify_signature": False}) + jws.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + options={"verify_signature": False}, + ) assert jws.options["verify_signature"] @@ -88,7 +93,7 @@ def test_options_must_be_dict(self): pytest.raises((TypeError, ValueError), PyJWS, options=("something")) def test_encode_decode(self, jws, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jws_message = jws.encode(payload, secret, algorithm="HS256") decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) @@ -97,7 +102,7 @@ def test_encode_decode(self, jws, payload): def test_decode_fails_when_alg_is_not_on_method_algorithms_param( self, jws, payload ): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jws_token = jws.encode(payload, secret, algorithm="HS256") jws.decode(jws_token, secret, algorithms=["HS256"]) @@ -105,17 +110,17 @@ def test_decode_fails_when_alg_is_not_on_method_algorithms_param( jws.decode(jws_token, secret, algorithms=["HS384"]) def test_decode_works_with_unicode_token(self, jws): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" unicode_jws = ( - "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" - ".eyJoZWxsbyI6ICJ3b3JsZCJ9" - ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + ".eyJoZWxsbyI6IndvcmxkIn0" + ".IjD2VRI4XN7tpFko0uxzudU6FjB_0B3r1umZzBX3XH8" ) jws.decode(unicode_jws, secret, algorithms=["HS256"]) def test_decode_missing_segments_throws_exception(self, jws): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jws = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9" # Missing segment with pytest.raises(DecodeError) as context: @@ -126,7 +131,7 @@ def test_decode_missing_segments_throws_exception(self, jws): def test_decode_invalid_token_type_is_none(self, jws): example_jws = None - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as context: jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -136,7 +141,7 @@ def test_decode_invalid_token_type_is_none(self, jws): def test_decode_invalid_token_type_is_int(self, jws): example_jws = 123 - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as context: jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -145,7 +150,7 @@ def test_decode_invalid_token_type_is_int(self, jws): assert "Invalid token type" in str(exception) def test_decode_with_non_mapping_header_throws_exception(self, jws): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jws = ( "MQ" # == 1 ".eyJoZWxsbyI6ICJ3b3JsZCJ9" @@ -159,19 +164,19 @@ def test_decode_with_non_mapping_header_throws_exception(self, jws): assert str(exception) == "Invalid header string: must be a json object" def test_encode_default_algorithm(self, jws, payload): - msg = jws.encode(payload, "secret") - decoded = jws.decode_complete(msg, "secret", algorithms=["HS256"]) - assert decoded == { - "header": {"alg": "HS256", "typ": "JWT"}, - "payload": payload, - "signature": ( - b"H\x8a\xf4\xdf3:\xe1\xac\x16E\xd3\xeb\x00\xcf\xfa\xd5\x05\xac" - b"e\xc8@\xb6\x00\xd5\xde\x9aa|s\xcfZB" - ), - } + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + msg = jws.encode(payload, secret) + decoded = jws.decode_complete(msg, secret, algorithms=["HS256"]) + + # Verify header and payload are correct + assert decoded["header"] == {"alg": "HS256", "typ": "JWT"} + assert decoded["payload"] == payload + # Verify signature exists and is bytes + assert isinstance(decoded["signature"], bytes) + assert len(decoded["signature"]) > 0 def test_encode_algorithm_param_should_be_case_sensitive(self, jws, payload): - jws.encode(payload, "secret", algorithm="HS256") + jws.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithm="HS256") with pytest.raises(NotImplementedError) as context: jws.encode(payload, None, algorithm="hs256") @@ -210,19 +215,18 @@ def test_encode_with_jwk(self, jws, payload): { "kty": "oct", "alg": "HS256", - "k": "c2VjcmV0", # "secret" + "k": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE", # 32-byte key in base64 } ) msg = jws.encode(payload, key=jwk) decoded = jws.decode_complete(msg, key=jwk, algorithms=["HS256"]) - assert decoded == { - "header": {"alg": "HS256", "typ": "JWT"}, - "payload": payload, - "signature": ( - b"H\x8a\xf4\xdf3:\xe1\xac\x16E\xd3\xeb\x00\xcf\xfa\xd5\x05\xac" - b"e\xc8@\xb6\x00\xd5\xde\x9aa|s\xcfZB" - ), - } + + # Verify header and payload are correct + assert decoded["header"] == {"alg": "HS256", "typ": "JWT"} + assert decoded["payload"] == payload + # Verify signature exists and is bytes + assert isinstance(decoded["signature"], bytes) + assert len(decoded["signature"]) > 0 def test_decode_algorithm_param_should_be_case_sensitive(self, jws): example_jws = ( @@ -232,14 +236,16 @@ def test_decode_algorithm_param_should_be_case_sensitive(self, jws): ) with pytest.raises(InvalidAlgorithmError) as context: - jws.decode(example_jws, "secret", algorithms=["hs256"]) + jws.decode( + example_jws, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["hs256"] + ) exception = context.value assert str(exception) == "Algorithm not supported" def test_bad_secret(self, jws, payload): - right_secret = "foo" - bad_secret = "bar" + right_secret = "foo" + "a" * 29 # 32 bytes total for HS256 + bad_secret = "bar" + "b" * 29 # 32 bytes total for HS256 but different jws_message = jws.encode(payload, right_secret) with pytest.raises(DecodeError) as excinfo: @@ -252,11 +258,11 @@ def test_bad_secret(self, jws, payload): assert "Signature verification failed" == str(excinfo.value) def test_decodes_valid_jws(self, jws, payload): - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jws = ( - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." - b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" + b"5o9-inkYItuy_w-qEr7izk2jekn-1W8oasoPNCy4p4s" ) decoded_payload = jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -264,11 +270,11 @@ def test_decodes_valid_jws(self, jws, payload): assert decoded_payload == payload def test_decodes_complete_valid_jws(self, jws, payload): - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jws = ( - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." - b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" + b"5o9-inkYItuy_w-qEr7izk2jekn-1W8oasoPNCy4p4s" ) decoded = jws.decode_complete(example_jws, example_secret, algorithms=["HS256"]) @@ -277,8 +283,8 @@ def test_decodes_complete_valid_jws(self, jws, payload): "header": {"alg": "HS256", "typ": "JWT"}, "payload": payload, "signature": ( - b"\x80E\xb4\xa5\xd58\x93\x13\xed\x86;^\x85\x87a\xc4" - b"\x1ff0\xe1\x9a\x8e\xddq\x08\xa9F\x19p\xc9\xf0\xf3" + b'\xe6\x8f~\x8ay\x18"\xdb\xb2\xff\x0f\xaa\x12\xbe\xe2\xceM\xa3z' + b"I\xfe\xd5o(j\xca\x0f4,\xb8\xa7\x8b" ), } @@ -287,13 +293,13 @@ def test_decodes_with_jwk(self, jws, payload): { "kty": "oct", "alg": "HS256", - "k": "c2VjcmV0", # "secret" + "k": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE", # 32-byte key } ) example_jws = ( - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." - b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" + b"5o9-inkYItuy_w-qEr7izk2jekn-1W8oasoPNCy4p4s" ) decoded_payload = jws.decode(example_jws, jwk, algorithms=["HS256"]) @@ -305,13 +311,13 @@ def test_decodes_with_jwk_and_no_algorithm(self, jws, payload): { "kty": "oct", "alg": "HS256", - "k": "c2VjcmV0", # "secret" + "k": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE", # 32-byte key } ) example_jws = ( - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." - b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" + b"5o9-inkYItuy_w-qEr7izk2jekn-1W8oasoPNCy4p4s" ) decoded_payload = jws.decode(example_jws, jwk) @@ -323,13 +329,13 @@ def test_decodes_with_jwk_and_mismatched_algorithm(self, jws, payload): { "kty": "oct", "alg": "HS512", - "k": "c2VjcmV0", # "secret" + "k": "ZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZA", # 64-byte key for HS512 } ) example_jws = ( - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." - b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" + b"5o9-inkYItuy_w-qEr7izk2jekn-1W8oasoPNCy4p4s" ) with pytest.raises(InvalidAlgorithmError): @@ -381,11 +387,11 @@ def test_decodes_valid_rs384_jws(self, jws): assert json_payload == example_payload def test_load_verify_valid_jws(self, jws, payload): - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jws = ( b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." - b"SIr03zM64awWRdPrAM_61QWsZchAtgDV3pphfHPPWkI" + b"5o9-inkYItuy_w-qEr7izk2jekn-1W8oasoPNCy4p4s" ) decoded_payload = jws.decode( @@ -394,14 +400,14 @@ def test_load_verify_valid_jws(self, jws, payload): assert decoded_payload == payload def test_allow_skip_verification(self, jws, payload): - right_secret = "foo" + right_secret = "foo" + "a" * 29 # 32 bytes total jws_message = jws.encode(payload, right_secret) decoded_payload = jws.decode(jws_message, options={"verify_signature": False}) assert decoded_payload == payload def test_decode_with_optional_algorithms(self, jws): - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jws = ( b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." @@ -417,7 +423,7 @@ def test_decode_with_optional_algorithms(self, jws): ) def test_decode_no_algorithms_verify_signature_false(self, jws): - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jws = ( b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." @@ -431,7 +437,7 @@ def test_decode_no_algorithms_verify_signature_false(self, jws): ) def test_load_no_verification(self, jws, payload): - right_secret = "foo" + right_secret = "foo" + "a" * 29 # 32 bytes total jws_message = jws.encode(payload, right_secret) decoded_payload = jws.decode( @@ -444,46 +450,51 @@ def test_load_no_verification(self, jws, payload): assert decoded_payload == payload def test_no_secret(self, jws, payload): - right_secret = "foo" + right_secret = "foo" + "a" * 29 # 32 bytes total jws_message = jws.encode(payload, right_secret) - with pytest.raises(DecodeError): + with pytest.raises((DecodeError, InvalidKeyError)): # Accept both error types jws.decode(jws_message, algorithms=["HS256"]) def test_verify_signature_with_no_secret(self, jws, payload): - right_secret = "foo" + right_secret = "foo" + "a" * 29 # 32 bytes total jws_message = jws.encode(payload, right_secret) - with pytest.raises(DecodeError) as exc: + with pytest.raises((DecodeError, InvalidKeyError)) as exc: jws.decode(jws_message, algorithms=["HS256"]) - assert "Signature verification" in str(exc.value) + # Accept either error message + assert "Signature verification" in str( + exc.value + ) or "HMAC key must be at least" in str(exc.value) def test_verify_signature_with_no_algo_header_throws_exception(self, jws, payload): example_jws = b"e30.eyJhIjo1fQ.KEh186CjVw_Q8FadjJcaVnE7hO5Z9nHBbU8TgbhHcBY" with pytest.raises(InvalidAlgorithmError): - jws.decode(example_jws, "secret", algorithms=["HS256"]) + jws.decode( + example_jws, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"] + ) def test_invalid_crypto_alg(self, jws, payload): with pytest.raises(NotImplementedError): - jws.encode(payload, "secret", algorithm="HS1024") + jws.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithm="HS1024") @no_crypto_required def test_missing_crypto_library_better_error_messages(self, jws, payload): with pytest.raises(NotImplementedError) as excinfo: - jws.encode(payload, "secret", algorithm="RS256") + jws.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithm="RS256") assert "cryptography" in str(excinfo.value) def test_unicode_secret(self, jws, payload): - secret = "\xc2" + secret = "\xc2" * 32 # Make it 32 bytes while preserving unicode nature jws_message = jws.encode(payload, secret) decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) assert decoded_payload == payload def test_nonascii_secret(self, jws, payload): - secret = "\xc2" # char value that ascii codec cannot decode + secret = "\xc2" * 32 # Make it 32 bytes while preserving non-ASCII nature jws_message = jws.encode(payload, secret) decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) @@ -491,7 +502,7 @@ def test_nonascii_secret(self, jws, payload): assert decoded_payload == payload def test_bytes_secret(self, jws, payload): - secret = b"\xc2" # char value that ascii codec cannot decode + secret = b"\xc2" * 32 # Make it 32 bytes while preserving bytes nature jws_message = jws.encode(payload, secret) decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) @@ -502,7 +513,7 @@ def test_bytes_secret(self, jws, payload): def test_sorting_of_headers(self, jws, payload, sort_headers): jws_message = jws.encode( payload, - key="\xc2", + key="\xc2" * 32, # Make it 32 bytes while preserving unicode headers={"b": "1", "a": "2"}, sort_headers=sort_headers, ) @@ -515,7 +526,7 @@ def test_decode_invalid_header_padding(self, jws): ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -528,7 +539,7 @@ def test_decode_invalid_header_string(self, jws): ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -541,7 +552,7 @@ def test_decode_invalid_payload_padding(self, jws): ".aeyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -554,7 +565,7 @@ def test_decode_invalid_crypto_padding(self, jws): ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".aatvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -574,7 +585,7 @@ def test_decode_with_algo_none_and_verify_false_should_pass(self, jws, payload): def test_get_unverified_header_returns_header_values(self, jws, payload): jws_message = jws.encode( payload, - key="secret", + key="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithm="HS256", headers={"kid": "toomanysecrets"}, ) @@ -695,16 +706,20 @@ def test_skip_check_signature(self, jws): ".eyJzb21lIjoicGF5bG9hZCJ9" ".4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZA" ) - jws.decode(token, "secret", options={"verify_signature": False}) + jws.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + options={"verify_signature": False}, + ) def test_decode_options_must_be_dict(self, jws, payload): - token = jws.encode(payload, "secret") + token = jws.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(TypeError): - jws.decode(token, "secret", options=object()) + jws.decode(token, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options=object()) with pytest.raises((TypeError, ValueError)): - jws.decode(token, "secret", options="something") + jws.decode(token, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options="something") def test_custom_json_encoder(self, jws, payload): class CustomJSONEncoder(json.JSONEncoder): @@ -716,10 +731,13 @@ def default(self, o): data = {"some_decimal": Decimal("2.2")} with pytest.raises(TypeError): - jws.encode(payload, "secret", headers=data) + jws.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", headers=data) token = jws.encode( - payload, "secret", headers=data, json_encoder=CustomJSONEncoder + payload, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + headers=data, + json_encoder=CustomJSONEncoder, ) header, *_ = token.split(".") @@ -730,7 +748,7 @@ def default(self, o): def test_encode_headers_parameter_adds_headers(self, jws, payload): headers = {"testheader": True} - token = jws.encode(payload, "secret", headers=headers) + token = jws.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", headers=headers) if not isinstance(token, str): token = token.decode() @@ -766,7 +784,9 @@ def test_encode_with_typ(self, jws): } """ token = jws.encode( - payload.encode("utf-8"), "secret", headers={"typ": "secevent+jwt"} + payload.encode("utf-8"), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + headers={"typ": "secevent+jwt"}, ) header = token[0 : token.index(".")].encode() @@ -777,7 +797,9 @@ def test_encode_with_typ(self, jws): assert header_obj["typ"] == "secevent+jwt" def test_encode_with_typ_empty_string(self, jws, payload): - token = jws.encode(payload, "secret", headers={"typ": ""}) + token = jws.encode( + payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", headers={"typ": ""} + ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) @@ -786,7 +808,9 @@ def test_encode_with_typ_empty_string(self, jws, payload): assert "typ" not in header_obj def test_encode_with_typ_none(self, jws, payload): - token = jws.encode(payload, "secret", headers={"typ": None}) + token = jws.encode( + payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", headers={"typ": None} + ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) @@ -796,7 +820,9 @@ def test_encode_with_typ_none(self, jws, payload): def test_encode_with_typ_without_keywords(self, jws, payload): headers = {"foo": "bar"} - token = jws.encode(payload, "secret", "HS256", headers, None) + token = jws.encode( + payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "HS256", headers, None + ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) @@ -807,17 +833,21 @@ def test_encode_with_typ_without_keywords(self, jws, payload): def test_encode_fails_on_invalid_kid_types(self, jws, payload): with pytest.raises(InvalidTokenError) as exc: - jws.encode(payload, "secret", headers={"kid": 123}) + jws.encode( + payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", headers={"kid": 123} + ) assert "Key ID header parameter must be a string" == str(exc.value) with pytest.raises(InvalidTokenError) as exc: - jws.encode(payload, "secret", headers={"kid": None}) + jws.encode( + payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", headers={"kid": None} + ) assert "Key ID header parameter must be a string" == str(exc.value) def test_encode_decode_with_detached_content(self, jws, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jws_message = jws.encode( payload, secret, algorithm="HS256", is_payload_detached=True ) @@ -825,7 +855,7 @@ def test_encode_decode_with_detached_content(self, jws, payload): jws.decode(jws_message, secret, algorithms=["HS256"], detached_payload=payload) def test_encode_detached_content_with_b64_header(self, jws, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # Check that detached content is automatically detected when b64 is false headers = {"b64": False} @@ -857,7 +887,7 @@ def test_decode_detached_content_without_proper_argument(self, jws): "." ".65yNkX_ZH4A_6pHaTL_eI84OXOHtfl4K0k5UnlXZ8f4" ) - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) @@ -868,7 +898,7 @@ def test_decode_detached_content_without_proper_argument(self, jws): ) def test_decode_warns_on_unsupported_kwarg(self, jws, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jws_message = jws.encode( payload, secret, algorithm="HS256", is_payload_detached=True ) @@ -885,7 +915,7 @@ def test_decode_warns_on_unsupported_kwarg(self, jws, payload): assert "foo" in str(record[0].message) def test_decode_complete_warns_on_unuspported_kwarg(self, jws, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jws_message = jws.encode( payload, secret, algorithm="HS256", is_payload_detached=True ) diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py index 2077b7b9..3ccd495d 100644 --- a/tests/test_api_jwt.py +++ b/tests/test_api_jwt.py @@ -46,11 +46,11 @@ def test_jwt_with_options(self): def test_decodes_valid_jwt(self, jwt): example_payload = {"hello": "world"} - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jwt = ( - b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" - b".eyJoZWxsbyI6ICJ3b3JsZCJ9" - b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + b".eyJoZWxsbyI6IndvcmxkIn0" + b".IjD2VRI4XN7tpFko0uxzudU6FjB_0B3r1umZzBX3XH8" ) decoded_payload = jwt.decode(example_jwt, example_secret, algorithms=["HS256"]) @@ -58,11 +58,11 @@ def test_decodes_valid_jwt(self, jwt): def test_decodes_complete_valid_jwt(self, jwt): example_payload = {"hello": "world"} - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jwt = ( - b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" - b".eyJoZWxsbyI6ICJ3b3JsZCJ9" - b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + b".eyJoZWxsbyI6IndvcmxkIn0" + b".IjD2VRI4XN7tpFko0uxzudU6FjB_0B3r1umZzBX3XH8" ) decoded = jwt.decode_complete(example_jwt, example_secret, algorithms=["HS256"]) @@ -70,18 +70,18 @@ def test_decodes_complete_valid_jwt(self, jwt): "header": {"alg": "HS256", "typ": "JWT"}, "payload": example_payload, "signature": ( - b'\xb6\xf6\xa0,2\xe8j"J\xc4\xe2\xaa\xa4\x15\xd2' - b"\x10l\xbbI\x84\xa2}\x98c\x9e\xd8&\xf5\xcbi\xca?" + b'"0\xf6U\x128\\\xde\xed\xa4Y(\xd2\xecs\xb9\xd5:\x160\x7f\xd0' + b"\x1d\xeb\xd6\xe9\x99\xcc\x15\xf7\\\x7f" ), } def test_load_verify_valid_jwt(self, jwt): example_payload = {"hello": "world"} - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jwt = ( - b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" - b".eyJoZWxsbyI6ICJ3b3JsZCJ9" - b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + b".eyJoZWxsbyI6IndvcmxkIn0" + b".IjD2VRI4XN7tpFko0uxzudU6FjB_0B3r1umZzBX3XH8" ) decoded_payload = jwt.decode( @@ -92,11 +92,10 @@ def test_load_verify_valid_jwt(self, jwt): def test_decode_invalid_payload_string(self, jwt): example_jwt = ( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aGVsb" - "G8gd29ybGQ.SIr03zM64awWRdPrAM_61QWsZchAtgDV" - "3pphfHPPWkI" + "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.aGVsbG" + "8gd29ybGQ.qIVQtvd8Pw-goEJSLpTB9nzXB7i3mCpHNkvqnz93WL0" ) - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" with pytest.raises(DecodeError) as exc: jwt.decode(example_jwt, example_secret, algorithms=["HS256"]) @@ -104,11 +103,11 @@ def test_decode_invalid_payload_string(self, jwt): assert "Invalid payload string" in str(exc.value) def test_decode_with_non_mapping_payload_throws_exception(self, jwt): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jwt = ( - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9." "MQ." # == 1 - "AbcSR3DWum91KOgfKxUHm78rLs_DrrZ1CrDgpUFFzls" + "FZMiRww3K5UDJYv6HDb2_qtB-SzP1gPyY8eWjAVv_Eg" ) with pytest.raises(DecodeError) as context: @@ -118,11 +117,11 @@ def test_decode_with_non_mapping_payload_throws_exception(self, jwt): assert str(exception) == "Invalid payload string: must be a json object" def test_decode_with_invalid_audience_param_throws_exception(self, jwt): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jwt = ( - "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" - ".eyJoZWxsbyI6ICJ3b3JsZCJ9" - ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + ".eyJoZWxsbyI6IndvcmxkIn0" + ".IjD2VRI4XN7tpFko0uxzudU6FjB_0B3r1umZzBX3XH8" ) with pytest.raises(TypeError) as context: @@ -132,11 +131,11 @@ def test_decode_with_invalid_audience_param_throws_exception(self, jwt): assert str(exception) == "audience must be a string, iterable or None" def test_decode_with_nonlist_aud_claim_throws_exception(self, jwt): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJoZWxsbyI6IndvcmxkIiwiYXVkIjoxfQ" # aud = 1 - ".Rof08LBSwbm8Z_bhA2N3DFY-utZR1Gi9rbIS5Zthnnc" + ".DbtPDOmDfdcuehvS4QoOHFh-jji0ISAw0Yd-RIswWf4" ) with pytest.raises(InvalidAudienceError) as context: @@ -151,11 +150,11 @@ def test_decode_with_nonlist_aud_claim_throws_exception(self, jwt): assert str(exception) == "Invalid claim format in token" def test_decode_with_invalid_aud_list_member_throws_exception(self, jwt): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJoZWxsbyI6IndvcmxkIiwiYXVkIjpbMV19" - ".iQgKpJ8shetwNMIosNXWBPFB057c2BHs-8t1d2CCM2A" + ".K2pDEQ3U-3TdS4HD_tcI5dmQIAXvDd-fmW0Q6Z-y7W0" ) with pytest.raises(InvalidAudienceError) as context: @@ -175,7 +174,9 @@ def test_encode_bad_type(self, jwt): for t in types: pytest.raises( TypeError, - lambda t=t: jwt.encode(t, "secret", algorithms=["HS256"]), + lambda t=t: jwt.encode( + t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"] + ), ) def test_encode_with_non_str_iss(self, jwt): @@ -185,7 +186,7 @@ def test_encode_with_non_str_iss(self, jwt): { "iss": 123, }, - key="secret", + key="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ) def test_encode_with_typ(self, jwt): @@ -205,7 +206,10 @@ def test_encode_with_typ(self, jwt): }, } token = jwt.encode( - payload, "secret", algorithm="HS256", headers={"typ": "secevent+jwt"} + payload, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + algorithm="HS256", + headers={"typ": "secevent+jwt"}, ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) @@ -215,32 +219,36 @@ def test_encode_with_typ(self, jwt): assert header_obj["typ"] == "secevent+jwt" def test_decode_raises_exception_if_exp_is_not_int(self, jwt): - # >>> jwt.encode({'exp': 'not-an-int'}, 'secret') + # >>> jwt.encode({'exp': 'not-an-int'}, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." "eyJleHAiOiJub3QtYW4taW50In0." - "P65iYgoHtBqB07PMtBSuKNUEIPPPfmjfJG217cEE66s" + "L5fBBapxrffCT4czpPFm2F9NeR0uTD25Qm7auvewgn8" ) with pytest.raises(DecodeError) as exc: - jwt.decode(example_jwt, "secret", algorithms=["HS256"]) + jwt.decode( + example_jwt, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"] + ) assert "exp" in str(exc.value) def test_decode_raises_exception_if_iat_is_not_int(self, jwt): - # >>> jwt.encode({'iat': 'not-an-int'}, 'secret') + # >>> jwt.encode({'iat': 'not-an-int'}, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." "eyJpYXQiOiJub3QtYW4taW50In0." - "H1GmcQgSySa5LOKYbzGm--b1OmRbHFkyk8pq811FzZM" + "fvrd35JEdbEzl-gjElD62axydQ_8e9FvTbmcdrUgFXA" ) with pytest.raises(InvalidIssuedAtError): - jwt.decode(example_jwt, "secret", algorithms=["HS256"]) + jwt.decode( + example_jwt, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"] + ) def test_decode_raises_exception_if_iat_is_greater_than_now(self, jwt, payload): payload["iat"] = utc_timestamp() + 10 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) with pytest.raises(ImmatureSignatureError): @@ -248,13 +256,13 @@ def test_decode_raises_exception_if_iat_is_greater_than_now(self, jwt, payload): def test_decode_works_if_iat_is_str_of_a_number(self, jwt, payload): payload["iat"] = "1638202770" - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) data = jwt.decode(jwt_message, secret, algorithms=["HS256"]) assert data["iat"] == "1638202770" def test_decode_raises_exception_if_nbf_is_not_int(self, jwt): - # >>> jwt.encode({'nbf': 'not-an-int'}, 'secret') + # >>> jwt.encode({'nbf': 'not-an-int'}, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." "eyJuYmYiOiJub3QtYW4taW50In0." @@ -262,20 +270,24 @@ def test_decode_raises_exception_if_nbf_is_not_int(self, jwt): ) with pytest.raises(DecodeError): - jwt.decode(example_jwt, "secret", algorithms=["HS256"]) + jwt.decode( + example_jwt, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"] + ) def test_decode_raises_exception_if_aud_is_none(self, jwt): - # >>> jwt.encode({'aud': None}, 'secret') + # >>> jwt.encode({'aud': None}, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') example_jwt = ( - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." "eyJhdWQiOm51bGx9." - "-Peqc-pTugGvrc5C8Bnl0-X1V_5fv-aVb_7y7nGBVvQ" + "lDjRLYgSGTJ8K-QzfQpHtqj8zBJJl8BkyIn2CYeAymU" + ) + decoded = jwt.decode( + example_jwt, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"] ) - decoded = jwt.decode(example_jwt, "secret", algorithms=["HS256"]) assert decoded["aud"] is None def test_encode_datetime(self, jwt): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" current_datetime = datetime.now(tz=timezone.utc) payload = { "exp": current_datetime, @@ -342,7 +354,7 @@ def test_decodes_valid_rs384_jwt(self, jwt): def test_decode_with_expiration(self, jwt, payload): payload["exp"] = utc_timestamp() - 1 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) with pytest.raises(ExpiredSignatureError): @@ -350,7 +362,7 @@ def test_decode_with_expiration(self, jwt, payload): def test_decode_with_notbefore(self, jwt, payload): payload["nbf"] = utc_timestamp() + 10 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) with pytest.raises(ImmatureSignatureError): @@ -358,7 +370,7 @@ def test_decode_with_notbefore(self, jwt, payload): def test_decode_skip_expiration_verification(self, jwt, payload): payload["exp"] = time.time() - 1 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) jwt.decode( @@ -370,7 +382,7 @@ def test_decode_skip_expiration_verification(self, jwt, payload): def test_decode_skip_notbefore_verification(self, jwt, payload): payload["nbf"] = time.time() + 10 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) jwt.decode( @@ -382,7 +394,7 @@ def test_decode_skip_notbefore_verification(self, jwt, payload): def test_decode_with_expiration_with_leeway(self, jwt, payload): payload["exp"] = utc_timestamp() - 2 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) # With 5 seconds leeway, should be ok @@ -399,7 +411,7 @@ def test_decode_with_expiration_with_leeway(self, jwt, payload): def test_decode_with_notbefore_with_leeway(self, jwt, payload): payload["nbf"] = utc_timestamp() + 10 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) # With 13 seconds leeway, should be ok @@ -410,54 +422,74 @@ def test_decode_with_notbefore_with_leeway(self, jwt, payload): def test_check_audience_when_valid(self, jwt): payload = {"some": "payload", "aud": "urn:me"} - token = jwt.encode(payload, "secret") - jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + audience="urn:me", + algorithms=["HS256"], + ) def test_check_audience_list_when_valid(self, jwt): payload = {"some": "payload", "aud": "urn:me"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", audience=["urn:you", "urn:me"], algorithms=["HS256"], ) def test_check_audience_none_specified(self, jwt): payload = {"some": "payload", "aud": "urn:me"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidAudienceError): - jwt.decode(token, "secret", algorithms=["HS256"]) + jwt.decode(token, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"]) def test_raise_exception_invalid_audience_list(self, jwt): payload = {"some": "payload", "aud": "urn:me"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidAudienceError): jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", audience=["urn:you", "urn:him"], algorithms=["HS256"], ) def test_check_audience_in_array_when_valid(self, jwt): payload = {"some": "payload", "aud": ["urn:me", "urn:someone-else"]} - token = jwt.encode(payload, "secret") - jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + audience="urn:me", + algorithms=["HS256"], + ) def test_raise_exception_invalid_audience(self, jwt): payload = {"some": "payload", "aud": "urn:someone-else"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidAudienceError): - jwt.decode(token, "secret", audience="urn-me", algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + audience="urn-me", + algorithms=["HS256"], + ) def test_raise_exception_audience_as_bytes(self, jwt): payload = {"some": "payload", "aud": ["urn:me", "urn:someone-else"]} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidAudienceError): - jwt.decode(token, "secret", audience=b"urn:me", algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + audience=b"urn:me", + algorithms=["HS256"], + ) def test_raise_exception_invalid_audience_in_array(self, jwt): payload = { @@ -465,20 +497,30 @@ def test_raise_exception_invalid_audience_in_array(self, jwt): "aud": ["urn:someone", "urn:someone-else"], } - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidAudienceError): - jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + audience="urn:me", + algorithms=["HS256"], + ) def test_raise_exception_token_without_issuer(self, jwt): issuer = "urn:wrong" payload = {"some": "payload"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(MissingRequiredClaimError) as exc: - jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + issuer=issuer, + algorithms=["HS256"], + ) assert exc.value.claim == "iss" @@ -487,67 +529,102 @@ def test_rasise_exception_on_partial_issuer_match(self, jwt): payload = {"iss": "urn:"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidIssuerError): - jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + issuer=issuer, + algorithms=["HS256"], + ) def test_raise_exception_token_without_audience(self, jwt): payload = {"some": "payload"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(MissingRequiredClaimError) as exc: - jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + audience="urn:me", + algorithms=["HS256"], + ) assert exc.value.claim == "aud" def test_raise_exception_token_with_aud_none_and_without_audience(self, jwt): payload = {"some": "payload", "aud": None} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(MissingRequiredClaimError) as exc: - jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + audience="urn:me", + algorithms=["HS256"], + ) assert exc.value.claim == "aud" def test_check_issuer_when_valid(self, jwt): issuer = "urn:foo" payload = {"some": "payload", "iss": "urn:foo"} - token = jwt.encode(payload, "secret") - jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + issuer=issuer, + algorithms=["HS256"], + ) def test_check_issuer_list_when_valid(self, jwt): issuer = ["urn:foo", "urn:bar"] payload = {"some": "payload", "iss": "urn:foo"} - token = jwt.encode(payload, "secret") - jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + issuer=issuer, + algorithms=["HS256"], + ) def test_raise_exception_invalid_issuer(self, jwt): issuer = "urn:wrong" payload = {"some": "payload", "iss": "urn:foo"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidIssuerError): - jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + issuer=issuer, + algorithms=["HS256"], + ) def test_raise_exception_invalid_issuer_list(self, jwt): issuer = ["urn:wrong", "urn:bar", "urn:baz"] payload = {"some": "payload", "iss": "urn:foo"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(InvalidIssuerError): - jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) + jwt.decode( + token, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + issuer=issuer, + algorithms=["HS256"], + ) def test_skip_check_audience(self, jwt): payload = {"some": "payload", "aud": "urn:me"} - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"verify_aud": False}, algorithms=["HS256"], ) @@ -557,10 +634,10 @@ def test_skip_check_exp(self, jwt): "some": "payload", "exp": datetime.now(tz=timezone.utc) - timedelta(days=1), } - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"verify_exp": False}, algorithms=["HS256"], ) @@ -570,12 +647,12 @@ def test_decode_should_raise_error_if_exp_required_but_not_present(self, jwt): "some": "payload", # exp not present } - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"require": ["exp"]}, algorithms=["HS256"], ) @@ -587,12 +664,12 @@ def test_decode_should_raise_error_if_iat_required_but_not_present(self, jwt): "some": "payload", # iat not present } - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"require": ["iat"]}, algorithms=["HS256"], ) @@ -604,12 +681,12 @@ def test_decode_should_raise_error_if_nbf_required_but_not_present(self, jwt): "some": "payload", # nbf not present } - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"require": ["nbf"]}, algorithms=["HS256"], ) @@ -624,7 +701,7 @@ def test_skip_check_signature(self, jwt): ) jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"verify_signature": False}, algorithms=["HS256"], ) @@ -634,10 +711,10 @@ def test_skip_check_iat(self, jwt): "some": "payload", "iat": datetime.now(tz=timezone.utc) + timedelta(days=1), } - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"verify_iat": False}, algorithms=["HS256"], ) @@ -647,10 +724,10 @@ def test_skip_check_nbf(self, jwt): "some": "payload", "nbf": datetime.now(tz=timezone.utc) + timedelta(days=1), } - token = jwt.encode(payload, "secret") + token = jwt.encode(payload, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") jwt.decode( token, - "secret", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", options={"verify_nbf": False}, algorithms=["HS256"], ) @@ -665,16 +742,20 @@ def default(self, o): data = {"some_decimal": Decimal("2.2")} with pytest.raises(TypeError): - jwt.encode(data, "secret", algorithms=["HS256"]) + jwt.encode(data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"]) - token = jwt.encode(data, "secret", json_encoder=CustomJSONEncoder) - payload = jwt.decode(token, "secret", algorithms=["HS256"]) + token = jwt.encode( + data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", json_encoder=CustomJSONEncoder + ) + payload = jwt.decode( + token, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", algorithms=["HS256"] + ) assert payload == {"some_decimal": "it worked"} def test_decode_with_verify_exp_option(self, jwt, payload): payload["exp"] = utc_timestamp() - 1 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) jwt.decode( @@ -694,7 +775,7 @@ def test_decode_with_verify_exp_option(self, jwt, payload): def test_decode_with_verify_exp_option_and_signature_off(self, jwt, payload): payload["exp"] = utc_timestamp() - 1 - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) jwt.decode( @@ -709,7 +790,7 @@ def test_decode_with_verify_exp_option_and_signature_off(self, jwt, payload): ) def test_decode_with_optional_algorithms(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) with pytest.raises(DecodeError) as exc: @@ -721,13 +802,13 @@ def test_decode_with_optional_algorithms(self, jwt, payload): ) def test_decode_no_algorithms_verify_signature_false(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) jwt.decode(jwt_message, secret, options={"verify_signature": False}) def test_decode_legacy_verify_warning(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) with pytest.deprecated_call(): @@ -745,13 +826,13 @@ def test_decode_legacy_verify_warning(self, jwt, payload): def test_decode_no_options_mutation(self, jwt, payload): options = {"verify_signature": True} orig_options = options.copy() - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) jwt.decode(jwt_message, secret, options=options, algorithms=["HS256"]) assert options == orig_options def test_decode_warns_on_unsupported_kwarg(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) with pytest.warns(RemovedInPyjwt3Warning) as record: @@ -760,7 +841,7 @@ def test_decode_warns_on_unsupported_kwarg(self, jwt, payload): assert "foo" in str(record[0].message) def test_decode_complete_warns_on_unsupported_kwarg(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret) with pytest.warns(RemovedInPyjwt3Warning) as record: @@ -769,7 +850,7 @@ def test_decode_complete_warns_on_unsupported_kwarg(self, jwt, payload): assert "foo" in str(record[0].message) def test_decode_strict_aud_forbids_list_audience(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" payload["aud"] = "urn:foo" jwt_message = jwt.encode(payload, secret) @@ -793,7 +874,7 @@ def test_decode_strict_aud_forbids_list_audience(self, jwt, payload): ) def test_decode_strict_aud_forbids_list_claim(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" payload["aud"] = ["urn:foo", "urn:bar"] jwt_message = jwt.encode(payload, secret) @@ -819,7 +900,7 @@ def test_decode_strict_aud_forbids_list_claim(self, jwt, payload): ) def test_decode_strict_aud_does_not_match(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" payload["aud"] = "urn:foo" jwt_message = jwt.encode(payload, secret) @@ -835,7 +916,7 @@ def test_decode_strict_aud_does_not_match(self, jwt, payload): ) def test_decode_strict_ok(self, jwt, payload): - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" payload["aud"] = "urn:foo" jwt_message = jwt.encode(payload, secret) @@ -853,14 +934,14 @@ def test_encode_decode_sub_claim(self, jwt): payload = { "sub": "user123", } - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) assert decoded["sub"] == "user123" def test_decode_without_and_not_required_sub_claim(self, jwt): - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode({}, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) @@ -868,7 +949,7 @@ def test_decode_without_and_not_required_sub_claim(self, jwt): assert "sub" not in decoded def test_decode_missing_sub_but_required_claim(self, jwt): - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode({}, secret, algorithm="HS256") with pytest.raises(MissingRequiredClaimError): @@ -880,7 +961,7 @@ def test_decode_invalid_int_sub_claim(self, jwt): payload = { "sub": 1224344, } - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(InvalidSubjectError): @@ -890,7 +971,7 @@ def test_decode_with_valid_sub_claim(self, jwt): payload = { "sub": "user123", } - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"], subject="user123") @@ -901,7 +982,7 @@ def test_decode_with_invalid_sub_claim(self, jwt): payload = { "sub": "user123", } - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(InvalidSubjectError) as exc_info: @@ -913,7 +994,7 @@ def test_decode_with_sub_claim_and_none_subject(self, jwt): payload = { "sub": "user789", } - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"], subject=None) @@ -925,7 +1006,7 @@ def test_encode_decode_with_valid_jti_claim(self, jwt): payload = { "jti": "unique-id-456", } - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) @@ -933,7 +1014,7 @@ def test_encode_decode_with_valid_jti_claim(self, jwt): def test_decode_missing_jti_when_required_claim(self, jwt): payload = {"name": "Bob", "admin": False} - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(MissingRequiredClaimError) as exc_info: @@ -944,7 +1025,7 @@ def test_decode_missing_jti_when_required_claim(self, jwt): assert "jti" in str(exc_info.value) def test_decode_missing_jti_claim(self, jwt): - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode({}, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) @@ -956,7 +1037,7 @@ def test_jti_claim_with_invalid_int_value(self, jwt): payload = { "jti": special_jti, } - secret = "your-256-bit-secret" + secret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(InvalidJTIError): @@ -991,3 +1072,109 @@ def test_validate_iss_with_non_str_issuer(self, jwt): } with pytest.raises(InvalidIssuerError): jwt._validate_iss(payload, issuer=123) + + +class TestKeyLengthValidationAPI: + """Test the new key length validation configuration API.""" + + def test_default_enforcement_enabled(self): + """Test that enforcement is enabled by default.""" + import jwt + + assert jwt.get_min_key_length_enforcement() is True + + def test_set_enforcement_to_false_shows_deprecation_warning(self): + """Test that disabling enforcement shows deprecation warning.""" + import warnings + + import jwt + + # Reset to True first + jwt.set_min_key_length_enforcement(True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + jwt.set_min_key_length_enforcement(False) + + assert len(w) >= 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "deprecated" in str(w[0].message).lower() + assert "PyJWT 3.0" in str(w[0].message) + + # Verify setting changed + assert jwt.get_min_key_length_enforcement() is False + + # Reset to default + jwt.set_min_key_length_enforcement(True) + + def test_enforcement_mode_affects_behavior(self): + """Test that enforcement mode actually affects key validation behavior.""" + import warnings + + import jwt + + weak_key = b"weak" # 4 bytes, below 32-byte minimum + payload = {"test": "data"} + + # Test strict mode (default) + jwt.set_min_key_length_enforcement(True) + with pytest.raises(jwt.InvalidKeyError, match="HMAC key must be at least"): + jwt.encode(payload, weak_key, algorithm="HS256") + + # Test warning mode + jwt.set_min_key_length_enforcement(False) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + token = jwt.encode(payload, weak_key, algorithm="HS256") + jwt.decode(token, weak_key, algorithms=["HS256"]) + + # Should have security warning + security_warnings = [ + warning for warning in w if "Security Warning" in str(warning.message) + ] + assert len(security_warnings) >= 1 + assert "HMAC key must be at least" in str(security_warnings[0].message) + + # Reset to default + jwt.set_min_key_length_enforcement(True) + + def test_strong_keys_work_in_both_modes(self): + """Test that strong keys work in both enforcement modes.""" + import warnings + + import jwt + + strong_key = b"a" * 32 # 32 bytes, meets minimum + payload = {"test": "data"} + + # Test in strict mode + jwt.set_min_key_length_enforcement(True) + token = jwt.encode(payload, strong_key, algorithm="HS256") + decoded = jwt.decode(token, strong_key, algorithms=["HS256"]) + assert decoded == payload + + # Test in warning mode (should not generate warnings for strong keys) + jwt.set_min_key_length_enforcement(False) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + token = jwt.encode(payload, strong_key, algorithm="HS256") + decoded = jwt.decode(token, strong_key, algorithms=["HS256"]) + + # Should not have security warnings for strong keys + security_warnings = [ + warning for warning in w if "Security Warning" in str(warning.message) + ] + assert len(security_warnings) == 0 + + # Reset to default + jwt.set_min_key_length_enforcement(True) + + def test_api_functions_are_exported(self): + """Test that the new API functions are properly exported.""" + import jwt + + # Test that functions exist and are callable + assert hasattr(jwt, "set_min_key_length_enforcement") + assert hasattr(jwt, "get_min_key_length_enforcement") + assert callable(jwt.set_min_key_length_enforcement) + assert callable(jwt.get_min_key_length_enforcement) diff --git a/tests/test_compressed_jwt.py b/tests/test_compressed_jwt.py index 21fac3fe..a38e8d9b 100644 --- a/tests/test_compressed_jwt.py +++ b/tests/test_compressed_jwt.py @@ -15,13 +15,13 @@ def _decode_payload(self, decoded): def test_decodes_complete_valid_jwt_with_compressed_payload(): # Test case from https://github.com/jpadilla/pyjwt/pull/753/files example_payload = {"hello": "world"} - example_secret = "secret" + example_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # payload made with the pako (https://nodeca.github.io/pako/) library in Javascript: # Buffer.from(pako.deflateRaw('{"hello": "world"}')).toString('base64') example_jwt = ( - b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" - b".q1bKSM3JyVeyUlAqzy/KSVGqBQA=" - b".08wHYeuh1rJXmcBcMrz6NxmbxAnCQp2rGTKfRNIkxiw=" + b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + b".q1bKSM3JyVeyUlAqzy_KSVGqBQA" + b".vkKB9BEuLsUnHbA6GBhk2MlmBRZuzH8Fo2GmBqzFdgc" ) decoded = CompressedPyJWT().decode_complete( example_jwt, example_secret, algorithms=["HS256"] @@ -31,7 +31,7 @@ def test_decodes_complete_valid_jwt_with_compressed_payload(): "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"\xbeB\x81\xf4\x11..\xc5'\x1d\xb0:\x18\x18d\xd8\xc9f\x05\x16n" + b"\xcc\x7f\x05\xa3a\xa6\x06\xac\xc5v\x07" ), } diff --git a/tests/test_jwt.py b/tests/test_jwt.py index 126fc9b7..857f8443 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -12,7 +12,7 @@ def test_encode_decode(): """ payload = {"iss": "jeff", "exp": utc_timestamp() + 15, "claim": "insanity"} - secret = "secret" + secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" jwt_message = jwt.encode(payload, secret, algorithm="HS256") decoded_payload = jwt.decode(jwt_message, secret, algorithms=["HS256"])