NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #37 most downloaded on PyPI
JSON Web Token implementation in Python
Last release 6 days ago
28 Sep 2026
Release timing varies
gaps range from 1 weeks to 1.2 years
Most releases are documented
notes for 41 of 56 stable releases
Nothing withdrawn
no release was ever pulled
16 years old
58 releases · first in 2011
See the 2.15.1 changelog for complete release details.
See the 2.15.1 changelog for complete release details.
See the 2.15.0 changelog for complete release details.
See the 2.15.0 changelog for complete release details.
See the 2.14.0 changelog for the complete release details and related security advisories.
One column per quarter.
See the 2.14.0 changelog for the complete release details and related security advisories.
PyJWT 2.13.0 — Security Release
This release bundles five security fixes plus three additional hardening / spec-compliance changes. We recommend all users upgrade.
GHSA-xgmm-8j9v-c9wx — JWK JSON accepted as HMAC secret (algorithm confusion). HMACAlgorithm.prepare_key previously rejected PEM- and SSH-formatted asymmetric keys but did not catch a JWK passed as a raw JSON string. In a verifier configured with both symmetric and asymmetric algorithms in algorithms=[…] and a raw-JSON JWK as the key, an attacker could forge HS256 tokens using the JWK text as the HMAC secret. The guard has been extended to reject any JWK-shaped JSON. Reported by @aradona91.
GHSA-jq35-7prp-9v3f — Algorithm allow-list bypass with PyJWK / PyJWKClient. When verifying with a PyJWK, the caller's algorithms=[…] allow-list was checked against the token header alg as a string only; actual verification used the algorithm bound to the PyJWK. An attacker who controlled a registered JWKS key could sign with one algorithm and advertise another on the header. PyJWT now requires the token header alg to match the PyJWK's algorithm before verification. Reported by @sushi-gif.
GHSA-w7vc-732c-9m39 — DoS via base64 decode of unused payload segment when b64=false. For detached-payload JWS (b64=false), the compact-form payload segment was base64-decoded before being discarded in favor of the caller-supplied detached_payload. An attacker could inflate the unused segment to force CPU + memory cost without holding a valid signature. The segment is now required to be empty per RFC 7515 Appendix F, and is no longer decoded. Reported by @thesmartshadow.
GHSA-993g-76c3-p5m4 — PyJWKClient accepts non-HTTP(S) URIs. PyJWKClient.fetch_data passed its URI to urllib.request.urlopen, which by default also handles file://, ftp://, and data: schemes. An application that fed an attacker-influenced URI into PyJWKClient could be coerced into reading local files or reaching other unintended schemes. PyJWKClient now rejects any URI whose scheme isn't http or https. Reported by @KEIJOT.
GHSA-fhv5-28vv-h8m8 — PyJWKClient cache wiped on fetch error. A finally-block put(jwk_set=None) cleared the JWK Set cache whenever a fetch raised, turning a transient JWKS-endpoint outage into application-wide auth failure. The cache write was moved into the success path; transient errors no longer evict valid cached keys. Reported by @eddieran.
HMACAlgorithm.prepare_key with InvalidKeyError instead of accepting them with only a warning. Defends against the os.getenv("JWT_SECRET", "") footgun. Thanks to @SnailSploit and @spartan8806 for the reports.options (including enforce_minimum_key_length) from PyJWT.decode through to PyJWS._verify_signature. The option was previously silently dropped between the two layers, so it only took effect when set on the PyJWT instance. Thanks to @WLUB for the report.b64=false: the encoder now auto-adds "b64" to crit, and the decoder rejects tokens that set b64=false without listing it in crit. Thanks to @MachineLearning-Nerd for the report.dev, docs, and tests package extras to dependency groups, by @kurtmckee in #1152.Most fixes are invisible to correctly-configured callers. A few behavioral changes you may encounter:
"" or b"" as a secret (often via a missing env var, e.g. os.getenv("JWT_SECRET", "")), encode/decode will now raise InvalidKeyError. This is the intended behavior — fix the configuration.PyJWK decoding now requires the token's alg to match the JWK's algorithm. Previously a mismatch was silently honored if the header alg appeared in the allow-list. Tokens that relied on this mismatch will now fail with InvalidAlgorithmError.PyJWKClient now rejects non-HTTP(S) URIs at construction time. Tests or dev environments that fetched JWKS from file:// URIs need to switch to a local HTTP server or load the JWKS by other means (e.g. construct PyJWKSet.from_dict(...) directly).b64=false tokens are now strictly RFC 7515 / 7797 compliant. Tokens with a non-empty compact-form payload segment, or that omit "b64" from crit, will be rejected. PyJWT-produced tokens always satisfy both invariants, so round-trips through PyJWT are unaffected.enforce_minimum_key_length set per-call now takes effect. Callers who passed options={"enforce_minimum_key_length": True} to jwt.decode() previously got no enforcement; they will now get InvalidKeyError on undersized keys, as documented.Full changelog: 2.12.1...2.13.0
Add typing_extensions dependency for Python < 3.11 by @jpadilla in #1151
Full Changelog: 2.12.0...2.12.1
Validate the crit (Critical) Header Parameter defined in RFC 7515 §4.1.11. by @dmbs335 in GHSA-752w-5fwx-jx9f
Full Changelog: 2.11.0...2.12.0
Migrate from pep517 , which is deprecated, to build by @kurtmckee in #1108
options in decode, decode_complete; Improve docs by @pachewise in #1045algorithm=None to "none" by @qqii in #1056PyJWKClient.get_signing_key_from_jwt annotation by @khvn26 in #1048float instead of int for lifespan and timeout by @nikitagashkov in #1068SyntaxWarning caused by invalid escape sequences by @kurtmckee in #1103pep517, which is deprecated, to build by @kurtmckee in #1108Full Changelog: 2.10.1...2.11.0
Prevent partial matching of iss claim. Thanks @fabianbadoi ! (See: GHSA-75c5-xw7c-p5pm )
iss claim. Thanks @fabianbadoi! (See: GHSA-75c5-xw7c-p5pm)Full Changelog: 2.10.0...2.10.1
chore: use sequence for typing rather than list by @imnotjames in #970
iat exception docs by @pachewise in #974sub and jti claims for the token by @Divan009 in #1005Full Changelog: 2.9.0...2.10.0
[pre-commit.ci] pre-commit autoupdate by @pre-commit-ci in #905
Full Changelog: 2.8.0...2.9.0
Export PyJWKClientConnectionError class by @daviddavis in #887
strict_aud option by @woodruffw in #902Full Changelog: 2.7.0...2.8.0
Add classifier for Python 3.11 by @eseifert in #818
Algorithm.compute_hash_digest and use it to implement at_hash validation example by @sirosen in #775sort_headers parameter to api_jwt.encode by @evroon in #832_validate_iat validation by @Viicos in #847Algorithm an abstract base class by @Viicos in #845as_dict option to Algorithm.to_jwk by @fluxth in #881Full Changelog: 2.6.0...2.7.0
fix: version 2.5.0 heading typo by @c0state in #803
types-cryptography from crypto extra by @lautat in #805Full Changelog: 2.5.0...2.6.0
Emit a deprecation warning for unsupported kwargs by @sirosen in https://github.com/jpadilla/pyjwt/pull/776
Full Changelog: https://github.com/jpadilla/pyjwt/compare/2.4.0...2.5.0
[CVE-2022-29217] Prevent key confusion through non-blocklisted public key formats. https://github.com/jpadilla/pyjwt/security/advisories/GHSA-ffqj-6fq…
Full Changelog: https://github.com/jpadilla/pyjwt/compare/2.3.0...2.4.0
[pre-commit.ci] pre-commit autoupdate by @pre-commit-ci in https://github.com/jpadilla/pyjwt/pull/700
Full Changelog: https://github.com/jpadilla/pyjwt/compare/2.2.0...2.3.0
Complete jwt documentation by @johachi in https://github.com/jpadilla/pyjwt/pull/654
jwt documentation by @johachi in https://github.com/jpadilla/pyjwt/pull/654Full Changelog: https://github.com/jpadilla/pyjwt/compare/2.1.0...2.2.0
Allow claims validation without making JWT signature validation mandatory. #608
kty mandatory in JWK to be compliant with RFC7517. #624alg to be compliant with RFC7517. #624__init__ imports #620from_jwk() to Ed25519Algorithm #621to_jwk() to Ed25519Algorithm #643PyJWK and PyJWKSet #652Rename CHANGELOG.md to CHANGELOG.rst and include in docs #597
from_jwk() for all algorithms #598Drop deprecation warnings (#515) by @jpadilla
Introduce PyJWK, PyJWKSet, and PyJWKClient.
import jwt
from jwt import PyJWKClient
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA"
kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw"
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
jwks_client = PyJWKClient(url)
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="https://expenses-api",
options={"verify_exp": False},
)
print(data)
We've kept this around for a long time, mostly for environments that didn't allow installing cryptography.
Dropped the included cli entry point.
We no longer need to use mypy Python 2 compatibility mode (comments)
python_requires (#478) by @michael-ktox -e lint warnings and errors (#490) by @jdufresnedefault_backend() (#523) by @rohitkg98Thanks to all that helped made this release happen one way or another. Special shout out to @jdufresne for all the amazing work getting this project into tip-top shape.
Nothing published for this version
Nothing published for this version
Fix pytest deprecation warnings
Support for Python 3.7 #375 #379 #384
Reverse an unintentional breaking API change to .decode() #352
Note: I accidentally published v1.6.2 and removed it from PyPI, that's why the jump to v1.6.3
Note: I accidentally published v1.6.2 and removed it from PyPI, that's why the jump to v1.6.3
Audience parameter throws InvalidAudienceError when application does not specify an audience, but the token does. #336
InvalidAudienceError when application does not specify an audience, but the token does. #336Dropped support for python 2.6 and 3.3 #301
InvalidSignatureError instead of DecodeError #316Remove uses of deprecated functions from the cryptography package.
algorithms param to decode() only when verify param is True #281Ensure correct arguments order in decode super call [7c1e61d][7c1e61d]
Add deprecation warning when decoding without specifying algorithms [#277][277]
Add support for ECDSA public keys in RFC 4253 (OpenSSH) format #244
jwt to jwt-cli to avoid issues with the script clobbering the jwt module in some circumstances. #187Nothing published for this version
Nothing published for this version
Nothing published for this version
ECDSA (ES256, ES384, ES512) signatures are now being properly serialized [#158][
jwt.get_unverified_header() to parse and return the header portion of a token prior to signature verification.Deprecated usage of the .decode(..., verify=False) parameter.
[BUGFIX] Include jwt/contrib' andjwt/contrib/algorithms` when installing. Ref 882524d845349df532e2a96b30fbe7e74e6ff55c
jwt/contrib' andjwt/contrib/algorithms` when installing. Ref 882524d845349df532e2a96b30fbe7e74e6ff55cA security researcher has notified JSON Web Token library maintainers about a number of vulnerabilities allowing attackers to bypass the verification…
api.header. #85PyCrypto and ecdsa when cryptography isn't available. #103alg header. #110A security researcher has notified JSON Web Token library maintainers about a number of vulnerabilities allowing attackers to bypass the verification step. Read more about some of this issues here.
This release fixes the vulnerabilities reported, continue reading for details.
alg field in the token header.Attackers can craft a malicious token containing an arbitrary payload that passes the verification step.
Create a token with the header {"typ":"JWT","alg":"none"}. Include any payload. Do not include a signature (i.e. the token should end with a period). Note: some implementations include some basic but insufficient checking for a missing signature -- some minor fiddling may be required to produce an exploit.
alg field in the token header.If the system is expecting a token signed with one of the asymmetric algorithms, an attacker can bypass the verification step by knowing only the public key.
Create an HS256 token. Generate the HMAC signature using the literal bytes of the public key file (often in the PEM format). This will confuse the implementation into interpreting the public key file as an HMAC key.
This release was possible thanks to the awesome @mark-adams.
Nothing published for this version
Include LICENSE and AUTHORS in the release tarball. #94
The following exceptions have been marked for deprecation in favor of a renamed one to follow a better convention and will be removed in the next majo…
InvalidTokenError for invalid tokens. #60The following exceptions have been marked for deprecation in favor of a renamed one to follow a better convention and will be removed in the next major version release.
ExpiredSignature will be deprecated in favor of ExpiredSignatureError.InvalidAudience will be deprecated in favor of InvalidAudienceError.InvalidIssuer will be deprecated in favor of InvalidIssuerError.Thanks to @mark-adams and @wbolster for all the work and feedback that went into this release.
Switch from PyCrypto to cryptography.
Switch from PyCrypto to cryptography.
PR: #51 by @mark-adams
Allow using a custom JSON encoder in jwt.encode()
Allow using a custom JSON encoder in jwt.encode()
PR #49 by @defyrlt Ref #37
import json
import decimal
import jwt
class CustomJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, decimal.Decimal):
return float(o)
return super(CustomJSONEncoder, self).default(o)
data = {
'some_decimal': decimal.Decimal('2.2')
}
token = jwt.encode(data, 'secret', json_encoder=CustomJSONEncoder)
Let header() support unicode input, like decode(). #48
header() support unicode input, like decode(). #48Add support for the "nbf" (Not Before) Claim by @skion.
Allow keys of type "bytes" in Python 3 by @cjlarose
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →