Skip to content
ecdsa.com
Python

PyJWT

José Padilla's PyJWT, the most used JWT library for Python: an explicit algorithms list is mandatory, alg: none never verifies, and public keys are refused as HMAC secrets — but expiry is optional and leeway is zero.
License
MIT
Checked version
2.15.1 ·
Source
github.com/jpadilla/pyjwt
Documentation
pyjwt.readthedocs.io/en/stable/

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512ES256KEdDSA

PyJWT encodes and decodes JSON Web Tokens with jwt.encode() and jwt.decode(), and fetches signing keys from a JWKS endpoint with PyJWKClient. Asymmetric algorithms come from the crypto extra, which pulls in pyca/cryptography; with it, 2.15.1 handled the HS, RS, PS and ES families, ES256K and EdDSA, but not the newer Ed25519 and Ed448 names, which raised NotImplementedError: Algorithm not supported.

Its verification defaults are strict. jwt.decode() without algorithms fails with It is required that you pass in a value for the "algorithms" argument, an unsigned alg: none token never verified — not even with algorithms=["none"] — and a PEM or OpenSSH public key used as an HS256 secret was refused with InvalidKeyError. ES256 is bound to P-256: signing with a P-384 key failed with a curve mismatch.

What it leaves to you: exp is optional, so a token without one decodes forever unless you list it under require in the options argument, and leeway defaults to zero, so servers a few seconds apart already disagree about expiry. Short HMAC secrets are accepted with only an InsecureKeyLengthWarning, which is easy to miss in logs — test a secret with the JWT secret checker.

ES256 signatures are the 64-byte raw r‖s form JWS requires; a token whose signature was assembled as DER — for example from a KMS signing call — fails with the generic Signature verification failed. Paste the token into the JWT debugger to see the signature length, and convert with the DER ⇄ raw converter.

The sample pins the algorithm, issuer and audience, requires exp and allows 30 seconds of drift — the four settings that make jwt.decode() safe to call on tokens from another service.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/pyjwt/run.sh ran PyJWT 2.15.1 on cryptography 50.0.2 in the container image python:3.14.8-slim-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512ES256KEdDSA

    HS, RS, PS and ES at 256/384/512 bits, ES256K and EdDSA signed and verified; Ed25519 and Ed448 raised NotImplementedError: Algorithm not supported.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, ES256K, EdDSA
    evidence jws-algorithms: Ed25519: NotImplementedError: Algorithm not supported
    evidence jws-algorithms: Ed448: NotImplementedError: Algorithm not supported
  • ES256 signature encoding

    raw

    ES256 signatures are 64 bytes of raw r‖s.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A token with a DER-encoded ES256 signature fails with InvalidSignatureError: Signature verification failed.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: InvalidSignatureError: Signature verification failed
  • Algorithm allow-list

    required

    jwt.decode() without algorithms raises DecodeError: It is required that you pass in a value for the "algorithms" argument when calling decode().

    Recorded by the check
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: DecodeError: It is required that you pass in a value for the "algorithms" argument when calling decode().
  • Unsigned tokens (alg none)

    rejected

    An unsigned token is refused with The specified alg value is not allowed against an ES256 key and with Signature verification failed even when algorithms=["none"]; only verify_signature: False reads it.

    Recorded by the check
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: with an ES256 key: InvalidAlgorithmError: The specified alg value is not allowed
    evidence jwt-alg-none: algorithms ['none'] and no key: InvalidSignatureError: Signature verification failed
    evidence jwt-alg-none: verify_signature False: returned claims
  • Public key used as an HMAC secret

    rejected

    A PEM or OpenSSH public key used as an HS256 secret raises InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: PEM public key as HS256 secret: InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
    evidence jwt-hmac-public-key: OpenSSH public key as HS256 secret: InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key raises The key's curve 'secp384r1' does not match the expected curve 'secp256r1' for this algorithm.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: InvalidKeyError: The key's curve 'secp384r1' does not match the expected curve 'secp256r1' for this algorithm
  • Default clock skew

    0s

    No leeway by default: a token that expired three seconds earlier was rejected.

    Recorded by the check
    fact jwt-clock-skew = 0s
  • Expiry claim required

    no

    A token without exp decoded successfully; require it by listing exp under require in the options argument.

    Recorded by the check
    fact jwt-exp-required = no
  • Short HMAC secret

    accepted-with-warning

    A 5-byte HMAC secret worked for signing and verifying, with an InsecureKeyLengthWarning as the only signal.

    Recorded by the check
    fact jwt-hmac-short-secret = accepted-with-warning
    evidence jwt-hmac-short-secret: 5-byte HMAC secret: accepted; warnings: InsecureKeyLengthWarning

Usage

Issue an ES256 token and decode it with the algorithm, issuer and audience pinned, exp required and 30 seconds of leeway.
import datetime

import jwt
from cryptography.hazmat.primitives.asymmetric import ec

key = ec.generate_private_key(ec.SECP256R1())
now = datetime.datetime.now(tz=datetime.timezone.utc)

token = jwt.encode(
    {"sub": "user-42", "iss": "https://auth.example.com", "aud": "api", "exp": now + datetime.timedelta(minutes=10)},
    key,
    algorithm="ES256",
)

# algorithms is mandatory; also require exp and allow a little clock drift.
claims = jwt.decode(
    token,
    key.public_key(),
    algorithms=["ES256"],
    audience="api",
    issuer="https://auth.example.com",
    leeway=30,
    options={"require": ["exp", "iss", "aud"]},
)
print("sub:", claims["sub"])

The snippet harness runs every sample verbatim in the container image python:3.14.8-slim-trixie, and the run fails unless the output matches.

Setup
pip install --quiet --root-user-action=ignore 'PyJWT[crypto]==2.15.1' cryptography==50.0.2
Command
python usage.py
Output must match (regular expression)
sub: user-42

Libraries