What a JWT Is
A JWT is a small, self-contained, signed token that carries claims (statements) about a user. The server signs it. Other servers can verify the signature to trust the contents without storing session state.
A JWT looks like three base64-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGljZSIsImV4cCI6MTcxNDg5NjAwMH0.M3v6X9...
[header].[payload].[signature]
Decoded:
// Header
{"alg": "HS256", "typ": "JWT"}
// Payload
{"sub": "12345", "name": "Alice", "exp": 1714896000}
// Signature: HMAC-SHA256(base64(header) + "." + base64(payload), secret)
Why JWTs Exist
Traditional sessions store state on the server: a session ID maps to user data in a database or Redis. Every request requires a session lookup.
JWTs are stateless. The token contains the user info. The server verifies the signature and trusts the contents. No database lookup. This makes JWTs popular in distributed systems where any server should be able to validate a token without coordination.
The Signature
The signature proves the JWT was issued by someone with the secret. Two flavors:
Symmetric (HMAC): issuer and verifier share the same secret. Simple, fast.
Asymmetric (RSA, ECDSA): issuer signs with a private key. Anyone can verify with the public key. Better for distributed systems where you don't want every service to have the signing key.
Common Claims
iss (issuer): who created the token.
sub (subject): who the token is about (typically user ID).
aud (audience): who the token is for.
exp (expiration): when the token expires (Unix timestamp).
iat (issued at): when the token was issued.
nbf (not before): token isn't valid before this time.
jti (JWT ID): unique ID for the token.
Plus any custom claims you want: roles, permissions, user metadata.
Where JWTs Fit
API authentication: client sends JWT in Authorization: Bearer <token> header. Server verifies and processes.
Single sign-on (SSO): one identity provider issues JWTs that many services accept.
OpenID Connect: the ID token in OIDC is a JWT.
Microservices: a JWT issued by an auth service flows through internal calls. Every service can verify without contacting the auth service.
The Hard Parts
Revocation
JWTs are valid until they expire. Even if the user logs out or you ban them, their JWT remains valid until expiration. There's no "delete the session" because there's no session.
Mitigations:
Short-lived access tokens: 5 to 15 minutes. Combined with longer refresh tokens that can be revoked.
Token blacklist: store revoked JWT IDs. Verify against blacklist on every request. Adds back the database lookup you were avoiding.
Rotate signing keys: changes invalidate all tokens. Heavy hammer.
Storage on the Client
Where does the client store the JWT?
localStorage: easy, but vulnerable to XSS. Any JS can read it.
httpOnly cookies: can't be read by JS. Safer against XSS. Vulnerable to CSRF without proper tokens.
Memory only: safest, but lost on page refresh. Combine with refresh tokens in cookies.
Common Pitfalls
Storing sensitive data in the payload: JWTs are signed but NOT encrypted. Anyone with the token can read its contents. Don't put passwords, secrets, or PII you wouldn't want logged.
The "alg: none" attack: some buggy libraries accept tokens with no signature. Always verify the algorithm.
Algorithm confusion: swapping HMAC for RSA tricks bad libraries into using the public key as the HMAC secret. Use libraries that prevent this.
Long expirations: 24-hour access tokens are everywhere and they're a leak risk.
Not verifying the issuer/audience: a JWT signed by service X but accepted by service Y is a problem. Always check iss and aud.
JWT vs Sessions: When to Use Which
JWTs make sense when:
You have many services that all need to validate identity.
You need stateless authentication for horizontal scaling.
You're integrating with OIDC or third-party auth.
Sessions are better when:
You have a single backend.
You need easy logout and instant revocation.
Simplicity matters more than statelessness.
Hybrid setups are common: short-lived JWTs for API auth, long-lived refresh tokens stored as opaque session IDs.
The One Thing to Remember
A JWT is a signed JSON object that proves "this server says these claims about you." The killer feature is statelessness; the killer flaw is revocation difficulty. Use them when the trade-off works for your system. Always pair with short expirations and refresh tokens. Use mature libraries. Don't put secrets in the payload.