mrkeyoor.com_
Sat 19 Sept 15:51 UTC
PyPISecurityupdated 19 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed pyjwtScreenshot of pyjwt documentation
Install✓ · 0.3s1 package on disk · 1 MB
Importimport jwt in 0.26s · pure Python · py.typed · requires Python >=3.9
Known vulns0(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.

API stability4/5PyJWT 2.x has kept its main surface small: `encode()`, `decode()`, claim options, exception classes, JWK objects, and `PyJWKClient`. Security releases intentionally narrow unsafe inputs. Version 2.13.0 now rejects empty HMAC keys, non-HTTP JWKS schemes, JWK and header algorithm mismatches, and invalid detached-payload forms. Correctly configured callers keep the same API, while code depending on those loose cases fails visibly and needs repair.
Docs4/5The stable manual separates basic usage, registered claim validation, algorithm configuration, RSA key handling, JWKS retrieval, exception classes, and the 1.x to 2.x migration. Examples show an explicit algorithm list and explain audience, issuer, leeway, and required claims. The reference cannot substitute for an application's security design: revocation, token purpose, scope evaluation, OAuth discovery, and authorization rules fall outside PyJWT and need separate documentation in each service.
Maintenance5/5GitHub records a push on August 24, 2026, 5,695 stars, 64 open issues and pull requests, and an unarchived repository. Release 2.13.0 arrived on May 21, 2026 with five named security fixes plus further key-length and detached-payload hardening. Its notes explain the vulnerable configurations and the expected upgrade failures, including empty secrets and local-file JWKS tests, which gives operators concrete checks to run.
Ecosystem5/5PyPI Stats counted 155,867,045 downloads in the latest week. The `jwt` API appears throughout Python web-framework and identity-provider examples, and `PyJWKClient` handles a common rotating-key verification path. The `crypto` extra connects it to maintained asymmetric primitives, while bundled typing helps checked Python code. Its reach does not expand its scope: OAuth flows, encrypted JWE content, login state, revocation, and authorization need other components.

Discussed on

  1. hnShow HN: My first article: SSO using Flask and selenium8 points

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.
Skip it if

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

PackageRegistryPick it when
AuthlibPyPIUse it when JWT work sits inside OAuth 2.0 or OpenID Connect protocol flows.
jwcryptoPyPIUse it when the application needs JWE encryption or lower-level JOSE key and message objects.
python-josePyPIUse 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.