You use JWTs for authentication in almost every modern web app. But most developers treat them as magic tokens without understanding whatโs inside. Letโs fix that.
A JWT Is Three Base64 Strings
A JWT looks like this:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
Three parts separated by dots: header.payload.signature. Each part is base64url-encoded (not encrypted โ anyone can decode it).
The Header
{
"alg": "HS256",
"typ": "JWT"
}
This tells you the signing algorithm. HS256 means HMAC-SHA256 (symmetric โ same secret for signing and verifying). RS256 means RSA-SHA256 (asymmetric โ private key signs, public key verifies).
The Payload
{
"sub": "1234567890",
"name": "Jane Developer",
"iat": 1710000000,
"exp": 1710086400
}
This is your data. Standard claims include:
subโ subject (usually user ID)iatโ issued at (Unix timestamp)expโ expiration timeissโ issueraudโ audience
You can add any custom claims you want. But remember: this is not encrypted. Anyone with the token can decode the payload. Donโt put passwords, credit card numbers, or secrets in here.
The Signature
This is the only part that provides security. For HS256:
HMAC-SHA256(
base64url(header) + "." + base64url(payload),
secret
)
The server takes the header and payload, concatenates them with a dot, and signs them with a secret key. The result is the signature.
When a token comes back, the server recomputes the signature using the same secret. If it matches, the token hasnโt been tampered with. If someone changes the payload (like changing "role": "user" to "role": "admin"), the signature wonโt match.
Why Itโs Not Encryption
This is the most common misconception. JWTs are signed, not encrypted.
- Signed = anyone can read the data, but only the server can create valid tokens
- Encrypted = nobody can read the data without the key
Base64 is an encoding, not encryption. You can decode any JWT payload right now at jwt.io. If you need the payload to be secret, you need JWE (JSON Web Encryption), which almost nobody uses.
The Auth Flow
1. User logs in with username/password
2. Server verifies credentials
3. Server creates JWT with user info, signs it
4. Server sends JWT to client
5. Client stores JWT (usually localStorage or httpOnly cookie)
6. Client sends JWT with every request (Authorization: Bearer <token>)
7. Server verifies signature, reads payload, knows who the user is
The key insight: the server doesnโt need to store sessions. The JWT itself contains all the user info. This is why JWTs are called โstatelessโ โ the server doesnโt need a database lookup to verify who you are.
Common Security Mistakes
Storing in localStorage
localStorage is accessible to any JavaScript on the page. One XSS vulnerability and an attacker has your token. Use httpOnly cookies instead โ JavaScript canโt access them.
Not validating expiration
Always check exp. A JWT without an expiration is valid forever. If it gets stolen, the attacker has permanent access.
Using alg: none
The JWT spec allows "alg": "none" โ no signature at all. Some libraries accept this by default. An attacker can craft a token with "alg": "none", remove the signature, and your server accepts it. Always reject none algorithm.
Symmetric secrets that are too short
For HS256, your secret needs to be at least 256 bits (32 bytes) of random data. "mysecret" can be brute-forced. Use a proper random key.
When to Use JWTs (And When Not To)
Good for: API authentication, microservice-to-microservice auth, short-lived access tokens, single sign-on.
Bad for: Session management where you need instant revocation (you canโt invalidate a JWT without a blocklist, which defeats the โstatelessโ purpose), storing sensitive data, long-lived tokens.
The sweet spot: short-lived JWTs (15 minutes) paired with refresh tokens stored server-side. You get the stateless benefits for most requests, with the ability to revoke access through the refresh token.
Related: What is JWT? ยท How HTTPS Keeps Your Data Safe ยท What is OAuth? ยท Free JWT Decoder tool