JWT explained: structure, security and best practices
What is a JWT, which algorithms exist and how to use them safely? Including the most common pitfalls.
JSON Web Tokens (JWT) are today's de-facto standard for stateless authentication in APIs. They are elegant, compact, and — when misused — an excellent security risk. This article covers structure, algorithms and the most important pitfalls.
What is a JWT?
A JWT is a string with three parts separated by dots: Header.Payload.Signature. Each part is Base64URL-encoded (like Base64, but without padding and with URL-safe characters).
Important: JWTs are signed, not encrypted. Anyone can read the payload by Base64-decoding it. Don't store passwords or secrets in there.
Example token decomposed
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNzMwMDAwMDAwLCJleHAiOjE3MzAwMDM2MDB9
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cHeader (decoded):
{
"alg": "HS256",
"typ": "JWT"
}Payload (decoded):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1730000000,
"exp": 1730003600
}The signature is HMAC_SHA256(base64url(header) + "." + base64url(payload), secret). On receipt, the server only verifies that signature — if it's valid, the token wasn't tampered with.
Signing algorithms: HS256 vs RS256 vs ES256
- HS256 (HMAC-SHA256) — symmetric. The same secret signs and verifies. Fast and simple, but anyone who can verify can also sign. Fine when a single service does both.
- RS256 (RSA-SHA256) — asymmetric. Private key signs, public key verifies. Ideal when several services need to verify tokens without being able to sign. Standard in OIDC/OAuth2.
- ES256 (ECDSA P-256) — like RS256 but with elliptic curves. Shorter signatures (~64 bytes instead of ~256), faster to verify. The recommended choice for new systems today.
Common claims
- iss (issuer) — who issued the token
- sub (subject) — who the token is about (user ID)
- aud (audience) — who the token is intended for
- exp (expiration) — Unix timestamp after which it's invalid
- iat (issued at) — when it was issued
- nbf (not before) — when it becomes valid
- jti (JWT ID) — unique ID, e.g. for revocation lists
At minimum, always set exp. Without it, a stolen token is valid forever.
Security pitfalls
1. alg: none
Older libraries accepted tokens with {"alg":"none"} and no signature. If your library still does: patch immediately. Always accept only a fixed algorithm on the server, never the one from the header.
2. Algorithm confusion
If the server expects RS256 but reads the algorithm from the header, an attacker can sign HS256 with the public RSA key as the secret — and the server accepts it. Fix: hard-code the algorithm.
3. No exp
Tokens without expiry are a security disaster. Default to 15 minutes to 1 hour.
4. JWT in LocalStorage
Tokens in LocalStorage are exfiltrated by any XSS bug. Better: HttpOnly cookie with Secure and SameSite=Lax or Strict. But add CSRF protection.
5. Weak HMAC key
At least 256 bits (32 bytes) of randomness, never a password or "secret". Tools like openssl rand -base64 32 handle it in one line.
Best practices
- Short lifetime for access tokens (5–60 minutes).
- Long lifetime only for refresh tokens, and rotate them on every use.
- Hard-code the algorithm — never trust the header.
- HttpOnly cookie instead of LocalStorage, or memory-only inside an SPA.
- JTI plus a server-side blocklist if you need revocation (otherwise the token is valid until
exp). - Never put sensitive data in the payload — it's just encoded, not encrypted.