JWT puts all the user data directly in the token instead of storing it server-side. No more session tables, no more Redis lookups, just three base64-encoded chunks that contain everything you need to know about a user. Sounds like the holy grail until you realize that revoking tokens is basically impossible.
The Three-Part Structure (That You'll Debug at 3AM)
A JWT is three base64 chunks separated by dots:
header.payload.signature
Here's what a real one looks like (go ahead, paste it into jwt.io to decode it):eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header - The "How to Verify This" Part
Tells you which algorithm to use. Don't trust this blindly or you'll get pwned:
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload - The Actual Data You Care About
This is your user info. Remember: it's just base64, not encrypted. Anyone can read it:
{
"sub": "user_12345",
"name": "Jane Developer",
"role": "admin",
"iat": 1725408179,
"exp": 1725409079
}
3. Signature - The "Trust Me Bro" Part
This proves the token wasn't tampered with (if you verify it correctly):
// What actually happens behind the scenes
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
process.env.JWT_SECRET // That secret you forgot to rotate for 2 years
)
The Standard Claims (And Why You'll Ignore Half of Them)
The RFC defines some standard claims that sound important but you'll probably only use a few. Here's what actually matters about these claims:
The ones you'll actually use:
sub
(Subject): User ID - this is your user primary keyexp
(Expiration): When this thing dies (Unix timestamp)iat
(Issued At): When you created it (mostly for debugging)
The ones you'll probably ignore:
iss
(Issuer): Which auth server made this (useful if you have multiple)aud
(Audience): Which app should accept this (good security practice, rarely implemented)nbf
(Not Before): Don't use before this time (why would you even...)
Custom stuff:
Throw whatever you want in there - user roles, permissions, coffee preference. Just remember everyone can read it.
Authentication vs Authorization (The Eternal Confusion)
Here's the thing everyone gets wrong: JWT doesn't authenticate shit. It just carries info about someone who was already authenticated. You still need a login page, you still need to check passwords somewhere.
JWT is for authorization - "this person is allowed to do X" not "this person is who they claim to be."
The real benefit is that any server can verify tokens without hitting a database. Scales like crazy, until you need to revoke someone's access and remember you can't actually delete JWTs.
The Debugging Reality Nobody Mentions
JWT debugging is a special kind of hell. Your token works in Postman but fails in the app. Is it the signature? The expiration? The algorithm? Good luck figuring it out from "invalid token" error messages.
Pro tip: jwt.io becomes your best friend. Bookmark it now.
Real debugging session: Spent 3 hours tracking down why tokens worked locally but failed in production. Turns out the Docker container had a different timezone, so iat
timestamps were off by 8 hours. The library was silently rejecting "future" tokens.