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.
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
| Install | ✓ · 0.3s | 5 packages on disk · 2 MB |
| Import | ✓ | import jose in 0.02s · pure Python · requires Python >=3.9 |
| Known vulns | 1 | (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.
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.
- 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.
- A clean dependency audit is required. pip-audit reported 1 known vulnerability in our fresh 3.5.0 environment, so this exact resolved graph fails that gate.
- Typed library code is mandatory. Version 3.5.0 has no py.typed marker, and our install found no package-supplied typing contract.
- Backend selection must be explicit. The README says the pure-Python backend is always installed and another available backend takes precedence at import time.
- Fast security releases are required. Version 3.3.0 in June 2021 was followed by 3.4.0 in February 2025, which carried fixes for two 2024 CVEs.
- You only need signed JWTs and no JWE. A full JOSE surface adds algorithms and code paths that the application must constrain and test.
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
| Package | Registry | Pick it when |
|---|---|---|
| PyJWT | PyPI | Choose it for signed JWT creation and validation without the wider JWE and general JWS surface. |
| joserfc | PyPI | Choose it for current full-JOSE APIs with explicit key objects and standards-focused documentation. |
| jwcrypto | PyPI | Choose it for JWS, JWE, and JWK work built around pyca/cryptography. |
| Authlib | PyPI | Choose 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.

