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"])