Skip to content
AldeaCode Logo
JWT Decoder / Python Developer 100% local

Decode JWT in Python: PyJWT, Authlib, and the verify=False trap

Python has two solid JWT libraries: PyJWT for the common case, Authlib when you need the full JOSE stack. Both follow RFC 7519 closely. The single most copy-pasted snippet on the internet for this task is also the most dangerous one.

PyJWT is the default

PyJWT (jpadilla/pyjwt) is the boring, correct choice. It handles HS256, RS256, ES256, and the rest of the algorithm landscape, with a small surface area. Verification looks like this:

```py import jwt

claims = jwt.decode( token, key=public_key, algorithms=["RS256"], audience="api.example.com", issuer="https://example.com", leeway=30, ) ```

The algorithms argument has been mandatory since PyJWT 2.0. Pass a single-element list when you know the issuer's algorithm; never accept what the token's own header advertises, because the token controls that field.

verify=False is a security smell

Half the snippets on Stack Overflow for "decode JWT python" pass options={"verify_signature": False} to skip the cryptographic check. Sometimes that is genuinely what you want (logging the claims of a malformed token in a postmortem). Almost never is it what you want at request time.

```py # Acceptable in a one-off debug REPL import jwt unsafe = jwt.decode(token, options={"verify_signature": False})

# Never in production code paths ```

If you find this line inside a request handler, treat it as a bug. The whole point of a JWT is the signature; reading the payload without checking it is equivalent to trusting any header the client sends.

JWKS rotation with PyJWT

When the IdP rotates keys you cannot hard-code the public key. Use PyJWKClient to fetch the JWKS, cache it, and pick the right key by kid:

```py from jwt import PyJWKClient import jwt

jwks = PyJWKClient("https://example.com/.well-known/jwks.json")

def verify(token: str) -> dict: signing_key = jwks.get_signing_key_from_jwt(token).key return jwt.decode( token, signing_key, algorithms=["RS256"], audience="api.example.com", issuer="https://example.com", leeway=30, ) ```

PyJWKClient caches the JWKS in-process. For multi-worker deployments (gunicorn, uvicorn workers) you may want a shared cache (Redis, memcached) to avoid each worker fetching independently on cold start.

Authlib when you need the rest of JOSE

If you only verify JWT, PyJWT is enough. If you also need to issue JWE (encrypted JWTs), parse JWS structures other than JWT, or implement OAuth2 / OIDC servers, Authlib is the broader toolkit. It is heavier than PyJWT and follows the JOSE specs more strictly.

For FastAPI a typical wiring is a dependency that returns the verified claims:

```py from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer

bearer = HTTPBearer()

def current_user(token = Depends(bearer)): try: return verify(token.credentials) except jwt.PyJWTError: raise HTTPException(status.HTTP_401_UNAUTHORIZED) ```

Routes then declare user = Depends(current_user) and never call jwt.decode directly. One verification path, one place to add audit logs.

Working example

python
import jwt
from jwt import PyJWKClient

jwks = PyJWKClient("https://example.com/.well-known/jwks.json")

def verify(token: str) -> dict:
    signing_key = jwks.get_signing_key_from_jwt(token).key
    return jwt.decode(
        token,
        signing_key,
        algorithms=["RS256"],
        audience="api.example.com",
        issuer="https://example.com",
        leeway=30,
    )

Just need the result?

When you just want to read what is in a token before writing the verification code, paste it into the browser-based JWT decoder. It splits header, payload and signature, shows the alg and kid, and never touches a server, so you can inspect production tokens without leaking them.

Open JWT Decoder →

Frequently asked questions

What is the difference between jwt.decode and jwt.get_unverified_header?

get_unverified_header reads only the header without checking the signature. It is the safe way to peek at the kid before calling decode. Never call it on the payload and call it carefully.

How do I verify a token signed by Auth0 or Cognito?

Both publish a JWKS at a well-known URL. Point PyJWKClient at it, set the issuer and audience to the values from your tenant, and use RS256 in algorithms. PyJWT handles the kid lookup automatically.

Why do I get InvalidAudienceError on a token that looks correct?

PyJWT compares audience exactly. If the token has aud as a list and you pass a string, it must match one entry; if you pass a list, every entry must match. Trim trailing slashes in URLs first, those are the silent killers.