Upgrading PyJWT 1 to 2
Breaking1.7.1 → 2.15.1Breaking10Security22Platform6Deprecated3New44Changed48Fixed44Other153
SecurityMedium · CVSS 5.3PyJWT: Unauthenticated RecursionError DoS in pre-verification payload parse (PyJWKClient.get_signing_key_from_jwt / verify_signature=False)GHSA-42vr-xj54-vc7v · CVE-2026-101918 ↗
BreakingUpgrade verdict
10 breaking changes and 6 platform requirement changes and 22 security fixes.
In plain terms
- Remove algorithm requirement for JWT API.
- Remove an unused variable from an example code block.
- Remove types-cryptography from crypto extra.
- Calls that passed arbitrary keyword arguments will no longer be accepted.
Must act Top 5 of 41 that need code or config changes
Requires Python >=3.9 from 2.10.0 (none stated before).
What you must change
Breaking10
Remove algorithm requirement for JWT API.
Remove an unused variable from an example code block.
Remove types-cryptography from crypto extra.
types-cryptographycryptoRemoves arbitrary kwargs.
Drops support for PyCrypto and ECDSA backends.
Drops the included CLI entry point.
Removes the CLI entry point.
Drops support for legacy contrib algorithms.
Drops deprecation warnings.
Removes the unused verify argument from PyJWS.decode().
PyJWS.decode()Security22
PyJWT: Unauthenticated RecursionError DoS in pre-verification payload parse (PyJWKClient.get_signing_key_from_jwt / verify_signature=False)
Medium · CVSS 5.3PyJWT is a Python implementation of JSON Web Token standards. From 2.1.0 until 2.15.0, PyJWT OKPAlgorithm.from_jwk in jwt/algorithms.py is affected because private-JWK import path does not compare the public key derived from d with x. This occurs when an OKP private JWK supplies non-corresponding x and d components. As a result, identity derived from x can differ from operations performed with d. Consequently, if an integration also accepts private key parameters from a proof header without rejecting them, an attacker may use a stolen sender-constrained token without the legitimate private key. This issue is fixed in version 2.15.0.
High · CVSS 8.2PyJWT: PyJWKClient still amplifies unauthenticated JWKS fetches on unknown kid values (incomplete fix of CVE-2026-48524)
Medium · CVSS 5.3PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header
Medium · CVSS 5.3PyJWT: PyJWK accepts empty HMAC keys, bypassing PyJWT's empty-key validation
High · CVSS 7.4PyJWT: PyJWKClient follows redirects when fetching JWKS
High · CVSS 7.4PyJWT: Asymmetric-PEM detection bypass: whitespace/line-ending-mutated public keys skip the HS/asymmetric confusion guard
Critical · CVSS 9.1PyJWT: Non-canonical signature segments enable raw-token revocation bypass
Medium · CVSS 4.8PyJWT: ReDoS vulnerability when calling the
Medium · CVSS 4.4is_pem_format function.PyJWT: Public keys in DER form are accepted as HMAC secrets, bypassing the CVE-2022-29217 guard
High · CVSS 7.4PyJWT BOM Bypass
High · CVSS 7.4PyJWT accepts public JWK containers as HMAC secrets
High · CVSS 7.4PyJWT: Malformed RSA JWK aborts parsing of an entire JWK Set
Medium · CVSS 5.9PyJWT: HS256/384/512 verify accepts empty HMAC key via oct JWK (PyJWK/PyJWKClient) enabling token forgery
High · CVSS 7.4Fixes algorithm confusion where a JWK JSON string was accepted as an HMAC secret, allowing forged HS256 tokens.
HMACAlgorithm.prepare_keyalgorithmsHigh · CVSS 7.4Fixes DoS via base64 decoding of the unused payload segment when b64=false; the segment must now be empty and is not decoded.
b64detached_payloadMedium · CVSS 5.3PyJWKClient now rejects URIs whose scheme is not http or https, preventing file, ftp and data scheme access.
PyJWKClientPyJWKClient.fetch_dataMedium · CVSS 4.2Fixes PyJWKClient wiping its JWK Set cache on fetch errors; transient outages no longer evict valid cached keys.
PyJWKClientLow · CVSS 3.7PyJWT is a JSON Web Token implementation in Python. From 2.9.0 to 2.12.1, there is a verifier-side algorithm allow-list bypass when jwt.decode() or jwt.decode_complete() are called with a PyJWK key. The token header alg is checked against the caller-supplied algorithms allow-list, but signature verification is performed with the algorithm bound to the PyJWK object instead of the header algorithm. An attacker who controls a registered JWK/JWKS private key can sign with a disallowed algorithm, advertise an allowed algorithm in the JWT header, and still be accepted. The issue affects the documented PyJWKClient.get_signing_key_from_jwt(...) flow. This vulnerability is fixed in 2.13.0.
Medium · CVSS 5.4Validate the crit (Critical) Header Parameter defined in RFC 7515 §4.1.11.
High · CVSS 7.5Prevent partial matching of iss claim.
issLow · CVSS 2.2Prevents key confusion through non-blocklisted public key formats (CVE-2022-29217).
Platform6
Drop support for Python 3.8.
Adds support for Python 3.10.
Uses the timezone package since Python 3.5+ is required.
timezoneDrops support for Python 2.
Adds support for Python 3.8.
Removes support for end-of-life Python 3.5.
Deprecations to plan for
Deprecated3
Migrate from pep517, which is deprecated, to build.
pep517buildEmits a deprecation warning for unsupported kwargs.
Raises DeprecationWarning for jwt.decode(verify=...).
jwt.decode()API changes
Generated by DepDelta from published Python source. Not mixed with prose notes.
- removed:
export ExpiredSignature - removed:
export InvalidAudience - removed:
export InvalidIssuer - added:
export InsecureKeyLengthWarning - added:
export InvalidKeyError - added:
export PyJWK - added:
export PyJWKClient - added:
export PyJWKClientConnectionError - added:
export PyJWKClientError - added:
export PyJWKError - added:
export PyJWKSet - added:
export PyJWKSetError - added:
export decode_complete - added:
export get_algorithm_by_name
- changed:
class Algorithm(object) → class Algorithm(ABC) - changed:
constructor ECAlgorithm(hash_alg) → constructor ECAlgorithm(hash_alg: type[hashes.HashAlgorithm], expected_curve: type[EllipticCurve] | None=…) -> None - changed:
constructor HMACAlgorithm(hash_alg) → constructor HMACAlgorithm(hash_alg: HashlibHash) -> None - changed:
constructor RSAAlgorithm(hash_alg) → constructor RSAAlgorithm(hash_alg: type[hashes.HashAlgorithm]) -> None - changed:
field ECAlgorithm.SHA256 → field ECAlgorithm.SHA256: ClassVar[type[hashes.HashAlgorithm]] - changed:
field ECAlgorithm.SHA384 → field ECAlgorithm.SHA384: ClassVar[type[hashes.HashAlgorithm]] - changed:
field ECAlgorithm.SHA512 → field ECAlgorithm.SHA512: ClassVar[type[hashes.HashAlgorithm]] - changed:
field HMACAlgorithm.SHA256 → field HMACAlgorithm.SHA256: ClassVar[HashlibHash] - changed:
field HMACAlgorithm.SHA384 → field HMACAlgorithm.SHA384: ClassVar[HashlibHash] - changed:
field HMACAlgorithm.SHA512 → field HMACAlgorithm.SHA512: ClassVar[HashlibHash] - changed:
field RSAAlgorithm.SHA256 → field RSAAlgorithm.SHA256: ClassVar[type[hashes.HashAlgorithm]] - changed:
field RSAAlgorithm.SHA384 → field RSAAlgorithm.SHA384: ClassVar[type[hashes.HashAlgorithm]] - changed:
field RSAAlgorithm.SHA512 → field RSAAlgorithm.SHA512: ClassVar[type[hashes.HashAlgorithm]] - changed:
function get_default_algorithms() → function get_default_algorithms() -> dict[str, Algorithm] - changed:
method Algorithm.prepare_key(key) → method Algorithm.prepare_key(key: Any) -> Any - changed:
method Algorithm.sign(msg, key) → method Algorithm.sign(msg: bytes, key: Any) -> bytes - changed:
method Algorithm.verify(msg, key, sig) → method Algorithm.verify(msg: bytes, key: Any, sig: bytes) -> bool - changed:
method ECAlgorithm.prepare_key(key) → method ECAlgorithm.prepare_key(key: AllowedECKeys | str | bytes) -> AllowedECKeys - changed:
method ECAlgorithm.sign(msg, key) → method ECAlgorithm.sign(msg: bytes, key: EllipticCurvePrivateKey) -> bytes - changed:
method ECAlgorithm.verify(msg, key, sig) → method ECAlgorithm.verify(msg: bytes, key: AllowedECKeys, sig: bytes) -> bool - changed:
method HMACAlgorithm.prepare_key(key) → method HMACAlgorithm.prepare_key(key: str | bytes) -> bytes - changed:
method HMACAlgorithm.sign(msg, key) → method HMACAlgorithm.sign(msg: bytes, key: bytes) -> bytes - changed:
method HMACAlgorithm.verify(msg, key, sig) → method HMACAlgorithm.verify(msg: bytes, key: bytes, sig: bytes) -> bool - changed:
method NoneAlgorithm.prepare_key(key) → method NoneAlgorithm.prepare_key(key: str | None) -> None - changed:
method NoneAlgorithm.sign(msg, key) → method NoneAlgorithm.sign(msg: bytes, key: None) -> bytes - changed:
method NoneAlgorithm.verify(msg, key, sig) → method NoneAlgorithm.verify(msg: bytes, key: None, sig: bytes) -> bool - changed:
method RSAAlgorithm.prepare_key(key) → method RSAAlgorithm.prepare_key(key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys - changed:
method RSAAlgorithm.sign(msg, key) → method RSAAlgorithm.sign(msg: bytes, key: RSAPrivateKey) -> bytes - changed:
method RSAAlgorithm.verify(msg, key, sig) → method RSAAlgorithm.verify(msg: bytes, key: RSAPublicKey, sig: bytes) -> bool - changed:
method RSAPSSAlgorithm.sign(msg, key) → method RSAPSSAlgorithm.sign(msg: bytes, key: RSAPrivateKey) -> bytes - changed:
method RSAPSSAlgorithm.verify(msg, key, sig) → method RSAPSSAlgorithm.verify(msg: bytes, key: RSAPublicKey, sig: bytes) -> bool - changed:
static method Algorithm.from_jwk(jwk) → static method Algorithm.from_jwk(jwk: str | JWKDict) -> Any - changed:
static method Algorithm.to_jwk(key_obj) → static method Algorithm.to_jwk(key_obj: Any, as_dict: Literal[True]) -> JWKDict | (key_obj: Any, as_dict: Literal[False]=…) -> str - changed:
static method HMACAlgorithm.from_jwk(jwk) → static method HMACAlgorithm.from_jwk(jwk: str | JWKDict) -> bytes - changed:
static method HMACAlgorithm.to_jwk(key_obj) → static method HMACAlgorithm.to_jwk(key_obj: str | bytes, as_dict: Literal[True]) -> JWKDict | (key_obj: str | bytes, as_dict: Literal[False]=…) -> str - changed:
static method RSAAlgorithm.from_jwk(jwk) → static method RSAAlgorithm.from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys - changed:
static method RSAAlgorithm.to_jwk(key_obj) → static method RSAAlgorithm.to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[True]) -> JWKDict | (key_obj: AllowedRSAKeys, as_dict: Literal[False]=…) -> str - added:
class OKPAlgorithm(Algorithm) - added:
constructor OKPAlgorithm(**kwargs: Any) -> None - added:
method Algorithm.check_crypto_key_type(key: PublicKeyTypes | PrivateKeyTypes) -> None - added:
method Algorithm.check_key_length(key: Any) -> str | None - added:
method Algorithm.compute_hash_digest(bytestr: bytes) -> bytes - added:
method HMACAlgorithm.check_key_length(key: bytes) -> str | None - added:
method OKPAlgorithm.prepare_key(key: AllowedOKPKeys | str | bytes) -> AllowedOKPKeys - added:
method OKPAlgorithm.sign(msg: str | bytes, key: Ed25519PrivateKey | Ed448PrivateKey) -> bytes - added:
method OKPAlgorithm.verify(msg: str | bytes, key: AllowedOKPKeys, sig: str | bytes) -> bool - added:
method RSAAlgorithm.check_key_length(key: AllowedRSAKeys) -> str | None - added:
static method ECAlgorithm.from_jwk(jwk: str | JWKDict) -> AllowedECKeys - added:
static method ECAlgorithm.to_jwk(key_obj: AllowedECKeys, as_dict: Literal[True]) -> JWKDict | (key_obj: AllowedECKeys, as_dict: Literal[False]=…) -> str - added:
static method NoneAlgorithm.from_jwk(jwk: str | JWKDict) -> NoReturn - added:
static method NoneAlgorithm.to_jwk(key_obj: Any, as_dict: bool=…) -> NoReturn - added:
static method OKPAlgorithm.from_jwk(jwk: str | JWKDict) -> AllowedOKPKeys - added:
static method OKPAlgorithm.to_jwk(key: AllowedOKPKeys, as_dict: Literal[True]) -> JWKDict | (key: AllowedOKPKeys, as_dict: Literal[False]=…) -> str - added:
variable AllowedECKeys - added:
variable AllowedECKeys: TypeAlias - added:
variable AllowedKeys - added:
variable AllowedKeys: TypeAlias - added:
variable AllowedOKPKeys - added:
variable AllowedOKPKeys: TypeAlias - added:
variable AllowedPrivateKeys - added:
variable AllowedPrivateKeys: TypeAlias - added:
variable AllowedPublicKeys - added:
variable AllowedPublicKeys: TypeAlias - added:
variable AllowedRSAKeys - added:
variable AllowedRSAKeys: TypeAlias
- added:
class PyJWK - added:
class PyJWKSet - added:
class PyJWTSetWithTimestamp - added:
constructor PyJWK(jwk_data: JWKDict, algorithm: str | None=…) -> None - added:
constructor PyJWKSet(keys: list[JWKDict]) -> None - added:
constructor PyJWTSetWithTimestamp(jwk_set: PyJWKSet) - added:
method PyJWTSetWithTimestamp.get_jwk_set() -> PyJWKSet - added:
method PyJWTSetWithTimestamp.get_timestamp() -> float - added:
property PyJWK.key_id -> str | None - added:
property PyJWK.key_type -> str | None - added:
property PyJWK.public_key_use -> str | None - added:
static method PyJWK.from_dict(obj: JWKDict, algorithm: str | None=…) -> PyJWK - added:
static method PyJWK.from_json(data: str, algorithm: str | None=…) -> PyJWK - added:
static method PyJWKSet.from_dict(obj: dict[str, Any]) -> PyJWKSet - added:
static method PyJWKSet.from_json(data: str) -> PyJWKSet
- changed:
class PyJWS(object) → class PyJWS - changed:
constructor PyJWS(algorithms=…, options=…) → constructor PyJWS(algorithms: Sequence[str] | None=…, options: SigOptions | None=…) -> None - changed:
method PyJWS.decode(jwt, key=…, verify=…, algorithms=…, options=…, **kwargs) → method PyJWS.decode(jwt: str | bytes, key: AllowedPublicKeys | PyJWK | str | bytes=…, algorithms: Sequence[str] | None=…, options: SigOptions | None=…, detached_payload: bytes | None=…, **kwargs: dict[str, Any]) -> Any - changed:
method PyJWS.encode(payload, key, algorithm=…, headers=…, json_encoder=…) → method PyJWS.encode(payload: bytes, key: AllowedPrivateKeys | PyJWK | str | bytes, algorithm: str | None=…, headers: dict[str, Any] | None=…, json_encoder: type[json.JSONEncoder] | None=…, is_payload_detached: bool=…, sort_headers: bool=…) -> str - changed:
method PyJWS.get_algorithms() → method PyJWS.get_algorithms() -> list[str] - changed:
method PyJWS.get_unverified_header(jwt) → method PyJWS.get_unverified_header(jwt: str | bytes) -> dict[str, Any] - changed:
method PyJWS.register_algorithm(alg_id, alg_obj) → method PyJWS.register_algorithm(alg_id: str, alg_obj: Algorithm) -> None - changed:
method PyJWS.unregister_algorithm(alg_id) → method PyJWS.unregister_algorithm(alg_id: str) -> None - added:
method PyJWS.decode_complete(jwt: str | bytes, key: AllowedPublicKeys | PyJWK | str | bytes=…, algorithms: Sequence[str] | None=…, options: SigOptions | None=…, detached_payload: bytes | None=…, **kwargs: dict[str, Any]) -> dict[str, Any] - added:
method PyJWS.get_algorithm_by_name(alg_name: str) -> Algorithm - added:
variable decode_complete - added:
variable get_algorithm_by_name
- removed:
field PyJWT.header_type - removed:
variable get_unverified_header - removed:
variable register_algorithm - removed:
variable unregister_algorithm - changed:
class PyJWT(PyJWS) → class PyJWT - changed:
method PyJWT.decode(jwt, key=…, verify=…, algorithms=…, options=…, **kwargs) → method PyJWT.decode(jwt: str | bytes, key: AllowedPublicKeys | PyJWK | str | bytes=…, algorithms: Sequence[str] | None=…, options: Options | None=…, verify: bool | None=…, detached_payload: bytes | None=…, audience: str | Iterable[str] | None=…, subject: str | None=…, issuer: str | Container[str] | None=…, leeway: float | timedelta=…, **kwargs: Any) -> dict[str, Any] - changed:
method PyJWT.encode(payload, key, algorithm=…, headers=…, json_encoder=…) → method PyJWT.encode(payload: dict[str, Any], key: AllowedPrivateKeyTypes, algorithm: str | None=…, headers: dict[str, Any] | None=…, json_encoder: type[json.JSONEncoder] | None=…, sort_headers: bool=…) -> str - added:
constructor PyJWT(options: Options | None=…) -> None - added:
method PyJWT.decode_complete(jwt: str | bytes, key: AllowedPublicKeyTypes=…, algorithms: Sequence[str] | None=…, options: Options | None=…, verify: bool | None=…, detached_payload: bytes | None=…, audience: str | Iterable[str] | None=…, issuer: str | Container[str] | None=…, subject: str | None=…, leeway: float | timedelta=…, **kwargs: Any) -> dict[str, Any] - added:
variable AllowedPrivateKeyTypes: TypeAlias - added:
variable AllowedPublicKeyTypes: TypeAlias - added:
variable decode_complete
- removed:
function bytes_from_int(val) - removed:
function constant_time_compare(val1, val2) - removed:
variable PY3 - removed:
variable binary_type - removed:
variable constant_time_compare - removed:
variable string_types - removed:
variable text_type
- removed:
class ECAlgorithm(Algorithm) - removed:
constructor ECAlgorithm(hash_alg) - removed:
field ECAlgorithm.SHA256 - removed:
field ECAlgorithm.SHA384 - removed:
field ECAlgorithm.SHA512 - removed:
method ECAlgorithm.prepare_key(key) - removed:
method ECAlgorithm.sign(msg, key) - removed:
method ECAlgorithm.verify(msg, key, sig)
- removed:
class RSAAlgorithm(Algorithm) - removed:
constructor RSAAlgorithm(hash_alg) - removed:
field RSAAlgorithm.SHA256 - removed:
field RSAAlgorithm.SHA384 - removed:
field RSAAlgorithm.SHA512 - removed:
method RSAAlgorithm.prepare_key(key) - removed:
method RSAAlgorithm.sign(msg, key) - removed:
method RSAAlgorithm.verify(msg, key, sig)
- removed:
variable ExpiredSignature - removed:
variable InvalidAudience - removed:
variable InvalidIssuer - changed:
constructor MissingRequiredClaimError(claim) → constructor MissingRequiredClaimError(claim: str) -> None - added:
class InvalidJTIError(InvalidTokenError) - added:
class InvalidSubjectError(InvalidTokenError) - added:
class MissingCryptographyError(PyJWKError) - added:
class PyJWKClientConnectionError(PyJWKClientError) - added:
class PyJWKClientError(PyJWTError) - added:
class PyJWKError(PyJWTError) - added:
class PyJWKSetError(PyJWTError)
- removed:
variable cryptography - removed:
variable ecdsa - changed:
function info() → function info() -> dict[str, dict[str, str]] - changed:
function main() → function main() -> None - added:
variable cryptography_version
- added:
class JWKSetCache - added:
constructor JWKSetCache(lifespan: float) -> None - added:
method JWKSetCache.get() -> Optional[PyJWKSet] - added:
method JWKSetCache.is_expired() -> bool - added:
method JWKSetCache.put(jwk_set: Optional[Union[PyJWKSet, dict[str, Any]]]) -> None
- added:
class PyJWKClient - added:
constructor PyJWKClient(uri: str, cache_keys: bool=…, max_cached_keys: int=…, cache_jwk_set: bool=…, lifespan: float=…, headers: dict[str, Any] | None=…, timeout: float=…, ssl_context: SSLContext | None=…, cooldown_duration: float=…) - added:
method PyJWKClient.fetch_data() -> Any - added:
method PyJWKClient.get_jwk_set(refresh: bool=…) -> PyJWKSet - added:
method PyJWKClient.get_signing_key(kid: str) -> PyJWK - added:
method PyJWKClient.get_signing_key_from_jwt(token: str | bytes) -> PyJWK - added:
method PyJWKClient.get_signing_keys(refresh: bool=…) -> list[PyJWK] - added:
static method PyJWKClient.match_kid(signing_keys: list[PyJWK], kid: str) -> PyJWK | None
- added:
class FullOptions(TypedDict) - added:
class Options(TypedDict) - added:
class SigOptions(TypedDict) - added:
field FullOptions.enforce_minimum_key_length: bool - added:
field FullOptions.require: list[str] - added:
field FullOptions.strict_aud: bool - added:
field FullOptions.verify_aud: bool - added:
field FullOptions.verify_exp: bool - added:
field FullOptions.verify_iat: bool - added:
field FullOptions.verify_iss: bool - added:
field FullOptions.verify_jti: bool - added:
field FullOptions.verify_nbf: bool - added:
field FullOptions.verify_signature: bool - added:
field FullOptions.verify_sub: bool - added:
field Options.enforce_minimum_key_length: bool - added:
field Options.require: list[str] - added:
field Options.strict_aud: bool - added:
field Options.verify_aud: bool - added:
field Options.verify_exp: bool - added:
field Options.verify_iat: bool - added:
field Options.verify_iss: bool - added:
field Options.verify_jti: bool - added:
field Options.verify_nbf: bool - added:
field Options.verify_signature: bool - added:
field Options.verify_sub: bool - added:
field SigOptions.enforce_minimum_key_length: bool - added:
field SigOptions.verify_signature: bool - added:
variable HashlibHash - added:
variable JWKDict
- removed:
function force_unicode(value) - removed:
function merge_dict(original, updates) - changed:
function base64url_decode(input) → function base64url_decode(input: Union[bytes, str]) -> bytes - changed:
function base64url_encode(input) → function base64url_encode(input: bytes) -> bytes - changed:
function bytes_to_number(string) → function bytes_to_number(string: bytes) -> int - changed:
function der_to_raw_signature(der_sig, curve) → function der_to_raw_signature(der_sig: bytes, curve: EllipticCurve) -> bytes - changed:
function force_bytes(value) → function force_bytes(value: Union[bytes, str]) -> bytes - changed:
function from_base64url_uint(val) → function from_base64url_uint(val: Union[bytes, str]) -> int - changed:
function number_to_bytes(num, num_bytes) → function number_to_bytes(num: int, num_bytes: int) -> bytes - changed:
function raw_to_der_signature(raw_sig, curve) → function raw_to_der_signature(raw_sig: bytes, curve: EllipticCurve) -> bytes - changed:
function to_base64url_uint(val) → function to_base64url_uint(val: int, *, bit_length: Optional[int]=…) -> bytes - added:
function bytes_from_int(val: int, *, bit_length: Optional[int]=…) -> bytes - added:
function is_pem_format(key: bytes) -> bool - added:
function is_ssh_key(key: bytes) -> bool
- added:
class InsecureKeyLengthWarning(UserWarning) - added:
class RemovedInPyjwt3Warning(DeprecationWarning)