pyjwt review
PyJWT creates and verifies signed JSON Web Tokens in Python. It supports HMAC in the base install, adds RSA and elliptic-curve algorithms through the crypto extra, validates registered claims such as `exp`, `nbf`, `aud`, and `iss`, and can select public keys from an HTTPS JWKS endpoint. It does not supply login handlers, refresh-token storage, revocation, OAuth flows, or JWE encryption. Version 2.13.0 fixes five security problems, including JWK algorithm confusion and unsafe JWKS URL schemes, and our install found a 1 MB pure-Python package.
PyJWT 2.13.0 installed in 0.3 seconds and used 1 MB in our sandbox, with a working 0.26-second import and 0 pip-audit findings, so it is an inexpensive signed-token primitive. Use it only with a fixed algorithm allowlist and explicit required claims; choose Authlib for full identity protocols or jwcrypto for encryption.
We installed it
| Install | ✓ · 0.3s | 1 package on disk · 1 MB |
| Import | ✓ | import jwt in 0.26s · pure Python · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pyjwt install cleanly?
Yes. In a fresh container with an empty cache, pip install pyjwt finished in 0.3s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does pyjwt need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import jwt succeeded in 0.26s, and the package ships py.typed for type checkers.
pyjwt or Authlib: which should you use?
Authlib: Use it when JWT work sits inside OAuth 2.0 or OpenID Connect protocol flows. PyJWT 2.13.0 installed in 0.3 seconds and used 1 MB in our sandbox, with a working 0.26-second import and 0 pip-audit findings, so it is an inexpensive signed-token primitive.
When should you not use pyjwt?
You need a complete OAuth 2.0 or OpenID Connect client or server. Authlib covers discovery, grants, endpoints, and JOSE together.
Discussed on
Use it if
- A Python service needs to sign or verify JWTs with a fixed algorithm policy and explicit claim checks.
- Your identity provider publishes rotating signing keys from a trusted HTTPS JWKS URL.
- You want a small token primitive while the application continues to own users, sessions, scopes, and authorization.
- Tests need distinct exceptions for expired tokens, invalid audiences, bad issuers, missing claims, and signature failures.
- You need a complete OAuth 2.0 or OpenID Connect client or server. Authlib covers discovery, grants, endpoints, and JOSE together.
- Tokens must be encrypted with JWE. PyJWT implements signed JWTs and JWS, so use a broader JOSE package such as jwcrypto.
- Your verifier would trust the `alg` value read from an unverified token. PyJWT requires the application to supply a safe algorithm allowlist.
- Immediate logout or per-device revocation is mandatory and you will not operate server-side token state. A valid stateless JWT remains usable until its policy rejects it.
- The team expects decoding to decide authorization. PyJWT verifies token structure and configured claims; scopes, tenant membership, token type, and resource access stay in application code.
Setup reality
We installed PyJWT 2.13.0 in a clean Python 3.12 Bookworm sandbox in 0.3 seconds. The result was 1 package occupying 1 MB, with 2 direct dependencies and 0 vulnerabilities reported by pip-audit. It requires Python 3.9 or newer, is pure Python, and includes py.typed. import jwt succeeded in 0.26 seconds. Our package measurement could not identify a license. The distribution name is PyJWT; the import name is jwt, which is also used by a separate PyPI project.
HS256 works with the base install. Install PyJWT[crypto] for RSA, ECDSA, and other asymmetric algorithms, then load signing keys from a secret store or mounted file. Version 2.13.0 rejects an empty HMAC key with InvalidKeyError, so a missing environment variable now fails instead of producing a token with an empty secret. Set the algorithm in trusted configuration and never copy it from an unverified header.
jwt.decode() checks only the policy you give it. Pass expected audience and issuer values, and use options={'require': ['exp', 'sub']} when those claims must exist. Expiration validation cannot object to a missing exp unless it is required. Keep leeway small and tied to measured clock skew. Token decode does not revoke a jti, confirm a scope, or distinguish an access token from an ID token unless your application performs those checks.
Construct PyJWKClient once for a trusted HTTPS URL so fetched sets and signing keys can be cached. Version 2.13.0 rejects file:, ftp:, and data: JWKS locations, requires a JWK's algorithm to match the token header, and retains a valid cache after a failed refresh. Provider timeouts and key rotation still need operational handling. get_unverified_header() may help choose a key, but kid and every other returned value remain attacker-controlled until verification passes.
Patterns
Sign a short-lived HS256 token issue-hmac-token
from datetime import datetime, timedelta, timezone
import jwt
now = datetime.now(timezone.utc)
token = jwt.encode(
{'sub': 'user-42', 'iat': now, 'exp': now + timedelta(minutes=15)},
secret,
algorithm='HS256',
)Version 2.13.0 raises `InvalidKeyError` for an empty HMAC secret; fail configuration before reaching this call.
Verify signature, issuer, and audience verify-token-policy
claims = jwt.decode(
token,
secret,
algorithms=['HS256'],
audience='inventory-api',
issuer='https://identity.example.com/',
)The `algorithms` list must come from trusted service configuration, never from the token's unverified header.
Reject tokens missing required claims require-security-claims
claims = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience='inventory-api',
options={'require': ['exp', 'iss', 'sub']},
)Expiration checking validates `exp` when present; the `require` option is what rejects a token with no `exp` claim.
Separate expected token failures handle-validation-errors
try:
claims = jwt.decode(token, key, algorithms=['RS256'], audience='inventory-api')
except jwt.ExpiredSignatureError:
return 'expired'
except jwt.InvalidAudienceError:
return 'wrong-audience'
except jwt.InvalidTokenError:
return 'invalid'Specific validation exceptions inherit from `InvalidTokenError`, so place the broad handler last.
Sign with an RSA private key sign-rsa-token
from pathlib import Path
import jwt
private_key = Path('/run/secrets/jwt-signing.pem').read_text()
token = jwt.encode(
{'sub': 'service-a', 'aud': 'worker'},
private_key,
algorithm='RS256',
)RSA support comes from the `PyJWT[crypto]` extra; the base install does not include `cryptography`.
Resolve a provider's rotating key verify-with-jwks
import jwt
jwks = jwt.PyJWKClient('https://identity.example.com/.well-known/jwks.json')
signing_key = jwks.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience='inventory-api',
issuer='https://identity.example.com/',
)PyJWT 2.13.0 accepts only HTTP or HTTPS JWKS URLs and checks that the selected JWK algorithm matches the token header.
Permit a bounded clock difference allow-clock-skew
claims = jwt.decode(
token,
key,
algorithms=['RS256'],
audience='inventory-api',
leeway=5,
)A 5-second leeway relaxes time checks such as `exp` and `nbf`; it also extends the acceptance window by that amount.
Inspect the key identifier before verification read-unverified-header
header = jwt.get_unverified_header(token)
kid = header.get('kid')
if kid is None:
raise ValueError('token has no kid')The returned `kid`, `alg`, and other header values are attacker input until a signature is verified with an approved algorithm.
Read verified header and payload together decode-complete-token
result = jwt.decode_complete(
token,
key,
algorithms=['RS256'],
audience='inventory-api',
)
header = result['header']
claims = result['payload']`decode_complete()` verifies under the supplied policy before returning the header, payload, and signature sections.
Allow two intended recipients accept-several-audiences
claims = jwt.decode(
token,
key,
algorithms=['RS256'],
audience=['inventory-api', 'reporting-api'],
)Audience validation succeeds when the token audience matches one of the configured recipients; keep the list tied to actual services.
Inspect claims without trusting them decode-for-debugging
claims = jwt.decode(
token,
options={'verify_signature': False},
)
print(claims)This disables signature verification and is safe only for diagnostics; never authorize a request from these claims.
Apply an application revocation check check-token-id-revocation
claims = jwt.decode(
token, key, algorithms=['RS256'],
options={'require': ['exp', 'sub', 'jti']},
)
if await revoked_tokens.contains(claims['jti']):
raise PermissionError('token revoked')PyJWT validates the claim format but stores no revocation state; the application must check `jti` against its own data store.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| Authlib | PyPI | Use it when JWT work sits inside OAuth 2.0 or OpenID Connect protocol flows. |
| jwcrypto | PyPI | Use it when the application needs JWE encryption or lower-level JOSE key and message objects. |
| python-jose | PyPI | Use it when an existing codebase already depends on its JWT, JWS, JWE, and JWK API, after reviewing its active release line. |
More security guides
cryptography · jose · dompurify · requests-oauthlib · jsonwebtoken · oauthlib · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

