JSON Web Tokens are widely used for authentication, and widely misunderstood — knowing exactly what a JWT does and doesn’t guarantee prevents some genuinely common security mistakes.
# A JWT is three base64-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fw
# header.payload.signature
# header: {"alg": "HS256", "typ": "JWT"}
# payload: {"sub": "1234567890", "role": "admin", "exp": 1735689600}
# signature: cryptographically verifies the header+payload haven't been tampered with
The Critical Misunderstanding: JWTs Are Signed, Not Encrypted
The payload is only base64-encoded, not encrypted — anyone who intercepts a JWT can trivially decode and read its contents (try it: paste any JWT into jwt.io). The signature only proves the payload hasn’t been tampered with since it was signed; it does not hide the payload’s contents at all. Never put genuinely secret data (passwords, full credit card numbers) in a JWT payload.
# Common mistake: accepting the 'alg' from the token itself without validation # An attacker can craft a token with alg:"none" and no signature at all - # always explicitly specify and enforce the expected algorithm server-side import jwt decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) # explicit algorithm list # NOT: jwt.decode(token, SECRET_KEY, algorithms=jwt.get_unverified_header(token)['alg'])
Always set and enforce a reasonably short expiration (exp claim) — a stolen JWT is valid until it expires, and unlike a traditional server-side session, there’s no built-in way to instantly revoke one without maintaining a separate revocation list, which somewhat defeats the “stateless” appeal JWTs are chosen for in the first place.