Skip to content
ecdsa.com
Python

python-jose

python-jose, a JOSE library still common in FastAPI tutorials: optional algorithms, no PS or EdDSA support, ES256 tokens signed with P-384 keys, and an open advisory that lets a DER public key act as an HMAC secret.
License
MIT
Checked version
3.5.0 ·
Source
github.com/mpdavis/python-jose
Documentation
python-jose.readthedocs.io/en/latest/

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512ES256ES384ES512

python-jose implements JWS, JWE, JWK and JWT with pluggable backends — pure-Python ecdsa, pycryptodome or cryptography; the check installed the cryptography extra. Years of FastAPI and Flask tutorials made it widespread, but development is slow: 3.4.0 (February 2025) came almost four years after 3.3.0, and new projects usually choose PyJWT, jwcrypto or joserfc instead.

algorithms is optional in jwt.decode(), and without it the algorithm comes from the token header. Combined with advisory GHSA-3qf3-8w2g-rqmx (CVE-2026-85394), published in September 2026 and unfixed in 3.5.0, that is exploitable: the guard against using public keys as HMAC secrets recognises PEM and OpenSSH text, but not the same key as DER bytes. The check reproduced it — an HS256 token whose HMAC key was the DER public key verified against that key when algorithms was left out, and was rejected with The specified alg value is not allowed once algorithms=["ES256"] was passed. Always pass algorithms; keep public keys as PEM strings; and check secrets with the JWT secret checker.

The algorithm set is the oldest of the Python libraries: the HS, RS and ES families only. PS256, EdDSA and ES256K raise Algorithm … not supported., so tokens from providers that switched to Ed25519 or RSA-PSS cannot be verified. ES256 is not bound to P-256 either: a P-384 key produced a 96-byte signature under an ES256 header that python-jose verified but other libraries, such as jose and PyJWT, refuse.

ES256 signatures are 64 bytes of raw r‖s and a DER signature fails with Signature verification failed.; the JWT debugger shows which form a token carries. exp is optional and leeway is zero unless you set require_exp and leeway in options, as the sample does.

Unsigned tokens were rejected with Unable to find an algorithm for key, even with algorithms=["none"].

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/python-jose/run.sh ran python-jose 3.5.0 on Python 3.14.8; installed with the [cryptography] extra; EC key class from jose.backends.cryptography_backend 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

    HS256HS384HS512RS256RS384RS512ES256ES384ES512

    HS, RS and ES at 256/384/512 bits; PS256/384/512, ES256K, EdDSA, Ed25519 and Ed448 raised JWSError: Algorithm … not supported.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
    evidence jws-algorithms: PS256: JWSError: Algorithm PS256 not supported.
    evidence jws-algorithms: PS384: JWSError: Algorithm PS384 not supported.
    evidence jws-algorithms: PS512: JWSError: Algorithm PS512 not supported.
    evidence jws-algorithms: ES256K: JWSError: Algorithm ES256K not supported.
    evidence jws-algorithms: EdDSA: JWSError: Algorithm EdDSA not supported.
    evidence jws-algorithms: Ed25519: JWSError: Algorithm Ed25519 not supported.
    evidence jws-algorithms: Ed448: JWSError: Algorithm Ed448 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 DER-encoded ES256 signature fails with JWTError: Signature verification failed.

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

    from-key

    algorithms is optional: an ES256 token verified without it, and an RS256 token against the EC key failed only at signature verification.

    Recorded by the check
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: no algorithms argument: verified; RS256 token against the EC key: JWTError: Signature verification failed.
  • Unsigned tokens (alg none)

    rejected

    Unsigned tokens raise JWKError: Unable to find an algorithm for key, with or without algorithms=["none"].

    Recorded by the check
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: with the EC key: JWKError: Unable to find an algorithm for key
    evidence jwt-alg-none: algorithms ['none']: JWKError: Unable to find an algorithm for key
  • Public key used as an HMAC secret

    accepted

    PEM and OpenSSH public keys are refused as HMAC secrets, but the DER encoding of the same public key was accepted: an HS256 token keyed with it verified when algorithms was omitted (advisory GHSA-3qf3-8w2g-rqmx, open for 3.5.0).

    Recorded by the check
    fact jwt-hmac-public-key = accepted
    evidence jwt-hmac-public-key: DER public key bytes, no algorithms: verified
    evidence jwt-hmac-public-key: DER public key bytes, algorithms ['ES256']: JWTError: The specified alg value is not allowed
    evidence jwt-hmac-public-key: PEM public key, algorithms ['HS256']: JWKError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
    evidence jwt-hmac-public-key: PEM public key, no algorithms: JWKError: 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, algorithms ['HS256']: JWKError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
  • Curve checked against alg

    not-enforced

    Signing ES256 with a P-384 key succeeded and produced a 96-byte signature that python-jose itself verified.

    Recorded by the check
    fact jwt-ecdsa-curve-check = not-enforced
    evidence jwt-ecdsa-curve-check: signing allowed; verification: verified; signature length 96 bytes under an ES256 header
  • 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 unless require_exp is set.

    Recorded by the check
    fact jwt-exp-required = no

Usage

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

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

key = ec.generate_private_key(ec.SECP256R1())
private_pem = key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()).decode()
public_pem = key.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo).decode()

token = jwt.encode({"sub": "user-42", "aud": "api", "exp": int(time.time()) + 600}, private_pem, algorithm="ES256")

# Always pass algorithms: without it python-jose takes the algorithm from the token header.
claims = jwt.decode(token, public_pem, algorithms=["ES256"], audience="api", options={"require_exp": True, "leeway": 30})
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 'python-jose[cryptography]==3.5.0' cryptography==50.0.2
Command
python usage.py
Output must match (regular expression)
sub: user-42

Libraries