mrkeyoor.com_
Sun 20 Sept 17:48 UTC
PyPISecurityupdated 20 Sept 2026

python-jose review

python-jose implements JSON Web Token, Signature, Encryption, Key, and Algorithm operations behind the `jose` import. Most applications use jwt.encode() and jwt.decode(), while lower layers expose JWS, JWE, and JWK objects. The package can select pyca/cryptography, PyCryptodome, or a pure-Python RSA/ECDSA backend based on installed extras. Version 3.5.0 adds Python 3.12 and 3.13 support, raises the pyasn1 floor, allows private RSA keys in jwk.construct(), and removes sensitive key material from JWKError messages. Our clean audit still reported one known vulnerability.

Verdict

Do not start a new plain-JWT integration with python-jose while our 3.5.0 graph has a known audit finding and no bundled types. Existing JOSE users should pin the cryptography backend, lock algorithms and required claims, review the audit result, and plan a comparison with PyJWT, joserfc, or jwcrypto.

We installed it

Lab card: what happened when we installed python-joseScreenshot of python-jose documentation
Install✓ · 0.3s5 packages on disk · 2 MB
Importimport jose in 0.02s · pure Python · requires Python >=3.9
Known vulns1(pip-audit)

Answers from our run

Does python-jose install cleanly?

Yes. In a fresh container with an empty cache, pip install python-jose finished in 0.3s, leaving 5 packages and 2 MB on disk. pip-audit reported 1 known vulnerability.

What does python-jose need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import jose succeeded in 0.02s.

python-jose or PyJWT: which should you use?

PyJWT: Choose it for signed JWT creation and validation without the wider JWE and general JWS surface. Do not start a new plain-JWT integration with python-jose while our 3.5.0 graph has a known audit finding and no bundled types.

When should you not use python-jose?

You are choosing a JWT library for new code. PyJWT has a narrower JWT surface, bundled type information, and current framework tutorials built around it.

API stability4/5jwt.encode(), jwt.decode(), JWS, JWE, and JWK entry points have changed little across the 3.x line, which reduces migration pressure for old FastAPI and Flask code. Security hardening has intentionally rejected earlier behavior, such as signing with public keys in 3.4.0. Version 3.5.0 drops Python 3.8, changes dependency floors, and adds private-RSA construction support, so environment and edge-key tests still belong in upgrades.
Docs2/5The README clearly recommends the cryptography extra, describes backend precedence, and admits that unused native-backend dependencies remain installed. Read the Docs exposes module references and a short JWT example. It gives much less operational help for allowed algorithms, required claims, JWKS refresh, caching, JWE limits, backend verification, and error handling. Those details require docstrings, source reading, changelog review, and security guidance from the surrounding protocol.
Maintenance2/5Version 3.5.0 shipped in May 2025, and GitHub shows a later push on April 14, 2026, 1,756 stars, 120 open issues and pull requests, and an unarchived repository. The release adds current Python support and removes key data from errors. The concern is response history: more than three years separated 3.3.0 and 3.4.0, while fixes for CVE-2024-33663 and CVE-2024-33664 did not reach a release until February 2025.
Ecosystem4/5The provided latest-week count is 10,137,781 downloads, and many existing Python API examples still import `jose.jwt`. It covers far more than basic tokens and accepts common PEM and JWK material through several crypto backends. That reach has a cost: framework tutorials are moving toward PyJWT, a base install brings RSA and ECDSA dependencies even when another backend is preferred, and the application must supply JWKS transport and cache behavior.

Use it if

  • An existing application already depends on the `from jose import jwt` API and a migration needs its own review window.
  • The same codebase needs JWS or JWE operations in addition to ordinary signed JWT claims.
  • Tokens arrive as a provider JWKS dictionary and the existing decode path relies on python-jose's key selection.
  • Claim checks for expiry, not-before, audience, issuer, subject, JWT ID, and access-token hash need to stay in one familiar decoder.
Skip it if

Setup reality

We installed python-jose 3.5.0 in a fresh Python 3.12 Bookworm container. Installation took 0.3 seconds, left 5 packages, and used 2 MB. Package metadata contains 8 direct requirements across the base package and extras. It requires Python >=3.9, is pure Python, has no py.typed marker, and uses the MIT license. import jose succeeded in 0.02 seconds. pip-audit reported 1 known vulnerability, so this resolved environment does not pass a zero-finding security policy.

The README recommends python-jose[cryptography]. A plain install uses the native Python backend, and the base rsa, ecdsa, and pyasn1 dependencies remain installed even when cryptography is selected. Backend choice follows what imports successfully rather than an application setting. Add a startup assertion for the expected backend, pin the complete lockfile, and preserve the pip-audit finding in the deployment review instead of assuming the cryptography extra removes the vulnerable package.

JWT verification needs application policy. Always pass a fixed algorithms list, audience, and issuer. Claims such as exp are verified when present, but a missing claim is accepted unless its require_* option is enabled. leeway defaults to zero, so set a small deliberate allowance only if distributed clocks need it. Unverified headers and claims are routing hints controlled by the token sender; they cannot authorize a request.

python-jose accepts a JWKS dictionary but does not own HTTP caching or key-refresh policy. Cache provider keys, honor the provider's cache headers where possible, and retry once after an unknown kid instead of fetching on every request. JWE compression should remain off unless required; 3.4.0 capped decompressed content to fix CVE-2024-33664. Version 3.5.0 also stops placing sensitive key data in JWKError text, but logs should still avoid raw tokens and keys.

Patterns

Assert the cryptography backend at startup verify-backend

from jose.backends import RSAKey

if RSAKey.__module__ != 'jose.backends.cryptography_backend':
    raise RuntimeError(f'unexpected JOSE backend: {RSAKey.__module__}')

Install python-jose[cryptography] and fail closed if the deployment silently falls back to the native Python backend.

Sign a short-lived access token encode-access-token

from datetime import datetime, timedelta, timezone
from jose import jwt

now = datetime.now(timezone.utc)
claims = {
    'sub': 'user-4213',
    'iss': 'https://auth.example.com',
    'aud': 'orders-api',
    'iat': now,
    'nbf': now,
    'exp': now + timedelta(minutes=15),
}
token = jwt.encode(claims, private_key, algorithm='RS256')

Use timezone-aware datetimes and keep the private signing key outside application source.

Verify algorithm, issuer, and audience decode-with-policy

claims = jwt.decode(
    token,
    public_key,
    algorithms=['RS256'],
    issuer='https://auth.example.com',
    audience='orders-api',
)

Never derive the allowed algorithms list from the token header. It is application configuration.

Reject tokens missing required claims require-claims

claims = jwt.decode(
    token,
    public_key,
    algorithms=['RS256'],
    issuer='https://auth.example.com',
    audience='orders-api',
    options={
        'require_exp': True,
        'require_iat': True,
        'require_sub': True,
        'require_aud': True,
    },
)

Verification of a claim does not necessarily require its presence. Enable the matching require option for every claim the authorization model depends on.

Set a bounded clock allowance allow-clock-skew

claims = jwt.decode(
    token,
    public_key,
    algorithms=['RS256'],
    options={'leeway': 30},
)

Leeway applies to time validation in seconds. Keep it small because it extends the usable window around token boundaries.

Separate expiry, claims, and malformed tokens classify-token-error

from jose.exceptions import ExpiredSignatureError, JWTClaimsError, JWTError

try:
    claims = jwt.decode(token, key, algorithms=['RS256'], audience='orders-api')
except ExpiredSignatureError:
    raise Unauthorized('token expired')
except JWTClaimsError:
    raise Forbidden('token claims rejected')
except JWTError:
    raise Unauthorized('invalid token')

Catch specific subclasses first. Do not return raw exception messages or the token to clients or logs.

Verify against a cached JWKS select-jwks-key

header = jwt.get_unverified_header(token)
kid = header.get('kid')
jwks = key_cache.current()

if kid not in {item.get('kid') for item in jwks['keys']}:
    jwks = key_cache.refresh_once()

claims = jwt.decode(
    token, jwks, algorithms=['RS256'],
    issuer=issuer, audience=audience,
)

The header is attacker-controlled. Use kid only to locate a configured provider key, and rate-limit refreshes for unknown values.

Read a header for routing only inspect-without-trust

header = jwt.get_unverified_header(token)
claims_hint = jwt.get_unverified_claims(token)

provider = configured_issuers.get(claims_hint.get('iss'))
if provider is None:
    raise Unauthorized('unknown issuer')

No value returned by an unverified function is authenticated. Complete decode() before using claims for access control.

Sign bytes outside the JWT claim format sign-jws-payload

from jose import jws

signed = jws.sign(
    b'{"event":"order.created"}',
    secret,
    algorithm='HS256',
    headers={'kid': 'webhook-2026-08'},
)
payload = jws.verify(signed, secret, algorithms=['HS256'])

JWS verification returns payload bytes and does not apply JWT expiry, issuer, audience, or replay checks.

Encrypt a small payload with JWE encrypt-jwe

import os
from jose import jwe

key = os.urandom(32)
token = jwe.encrypt(
    b'private payload',
    key,
    algorithm='dir',
    encryption='A256GCM',
)
plaintext = jwe.decrypt(token, key)

Leave zip compression disabled unless protocol compatibility requires it. Version 3.4.0 added a decompression limit after CVE-2024-33664.

Build an RSA key from a JWK construct-rsa-jwk

from jose import jwk

key = jwk.construct(jwk_dict, algorithm='RS256')

Pass the expected algorithm from trusted configuration. Version 3.5.0 also permits private RSA key input here.

Validate an OIDC access-token hash verify-access-token-hash

claims = jwt.decode(
    id_token,
    provider_jwks,
    algorithms=['RS256'],
    audience=client_id,
    issuer=issuer,
    access_token=access_token,
    options={'verify_at_hash': True},
)

at_hash belongs to OpenID Connect token validation. It does not replace issuer, audience, nonce, state, or authorization-code flow checks.

Alternatives

PackageRegistryPick it when
PyJWTPyPIChoose it for signed JWT creation and validation without the wider JWE and general JWS surface.
joserfcPyPIChoose it for current full-JOSE APIs with explicit key objects and standards-focused documentation.
jwcryptoPyPIChoose it for JWS, JWE, and JWK work built around pyca/cryptography.
AuthlibPyPIChoose it when JOSE is one part of an OAuth 2.0 or OpenID Connect client or server implementation.

More security guides

cryptography · pyjwt · jose · requests-oauthlib · oauthlib · dompurify · 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.