JWT (JSON Web Tokens)
A JWT is a compact, signed token that carries claims about a user. Learn its three parts, why it is signed but not encrypted, and how to use it safely.
Prerequisites: Authentication vs Authorization
Learning objectives
- Break a JWT into its three Base64URL parts: header, payload, and signature.
- Explain that a JWT is signed for integrity and authenticity, not encrypted for secrecy.
- Identify the standard registered claims (iss, sub, aud, exp, iat, nbf) and when to use custom claims.
- Recognize common JWT pitfalls (alg:none, weak secrets, no expiry, unsafe storage) and how to avoid them.
Simple explanation
Think of a JWT like a festival wristband. When you buy a ticket at the gate, staff give you a wristband that says who you are and which areas you can enter. From then on you don't queue at the ticket office again — you just show the wristband and security waves you through. The wristband is *tamper-evident*: it has a special seal so nobody can fake one or change what's printed on it without it being obvious.
A JWT (JSON Web Token) works the same way. After you log in, the server hands you a signed token. On every later request you present that token, and the server trusts it — because the signature proves the server itself issued it and nobody altered it.
Technical explanation
A JWT is a single string with three parts separated by dots:
header.payload.signatureEach part is Base64URL-encoded (not encrypted — just encoded, so anyone can decode it).
- Header — a small JSON object describing the token, mainly the signing algorithm (
alg, e.g.HS256orRS256) and the type (typ: "JWT"). - Payload — a JSON object holding the claims: statements about the user and the token. *Registered* claims are standardized short names:
iss(issuer),sub(subject — usually the user id),aud(audience),exp(expiration time),iat(issued at),nbf(not valid before).
You can also add your own custom claims like role or email.
- Signature — the security guarantee. The server takes
base64url(header) + "." + base64url(payload)and signs it, either with a shared secret (HMAC, e.g. HS256) or a private key (RSA/ECDSA, e.g. RS256/ES256). On each request the server recomputes the signature and checks it matches.
The critical property: a JWT is signed, not encrypted. Signing protects integrity (the payload wasn't changed) and authenticity (a trusted party issued it). It does *not* hide the contents. Anyone who holds the token can Base64URL-decode the payload and read every claim.
Why it matters
JWTs enable stateless authentication. Because the token is self-contained — it carries the user id, roles, and expiry inside it — the server can verify a request just by checking the signature, without a database or session-store lookup. That makes horizontal scaling easy: any server instance can validate any token. This is why JWTs are the backbone of most modern APIs, single sign-on (SSO), and microservice-to-microservice auth.
How it works
A typical login-then-request flow:
- The user logs in with credentials; the server verifies them.
- The server builds a payload (
sub,exp, custom claims) and signs it, producing the JWT. - The token is sent to the client, which stores it (ideally an
httpOnlycookie). - On each subsequent request the client sends the token, usually as
Authorization: Bearer <token>. - The server verifies the signature and checks
exp/nbf/aud. If valid, it trusts the claims and processes the request — no session lookup needed. - When the token expires, the client obtains a new one (typically via a refresh token).
Real-world analogy
A JWT is like a sealed, signed concert ticket. Anyone can read what's printed on it — your seat, the date, your name — so you would never write your credit-card PIN on the ticket. But the venue's official seal (the signature) means the gate can instantly tell a genuine ticket from a forgery, without phoning head office (no database call). If someone tries to erase "balcony" and write "front row", the seal breaks and the ticket is rejected. And every ticket has a date on it (exp) so an old one won't get you in next year.
Important terminology
- Claim — a single statement inside the payload, e.g.
"sub": "user_42"or"role": "admin". - Signature — a cryptographic value over the header and payload that proves integrity and authenticity.
- HMAC vs RSA/ECDSA — HMAC (HS256) uses one shared secret for both signing and verifying; RSA/ECDSA (RS256/ES256) uses a private key to sign and a public key to verify, which is better when many parties must verify but only one issues.
- Stateless — the server stores no session; everything needed to authorize the request lives in the token itself.
- Bearer token — whoever *bears* (holds) the token can use it, which is exactly why transport (HTTPS) and safe storage matter.
Key definitions
- Token
- The compact header.payload.signature string a client presents to prove who it is on each request.
- Claim
- A single statement inside the payload, such as sub (subject), exp (expiry), or a custom role.
- Signature
- A cryptographic value over the header and payload that proves the token was not tampered with and was issued by a trusted party.
- Statelessness
- The server keeps no session; the self-contained token carries everything needed to authorize a request.
Must memorize
Must memorize
- A JWT has three Base64URL parts: header.payload.signature.
- A JWT is signed, not encrypted — anyone can read the payload, so never put secrets in it.
- The signature proves integrity and authenticity; it does not hide the contents.
- JWTs are hard to revoke — use short exp and a refresh-token flow.
Real-world example
Stateless API authentication
A React app logs the user in and receives a JWT. It sends the token as "Authorization: Bearer <token>" on every API call. Each backend instance verifies the signature and reads the sub and role claims to authorize the request — with no shared session store, so the API scales horizontally without sticky sessions.
Single sign-on (SSO) with RS256
An identity provider signs tokens with its private key. Many independent services verify those tokens using the provider’s public key (fetched via JWKS). Only the provider can issue tokens, but every service can verify them offline — a perfect fit for asymmetric (RS256/ES256) signing.
Code example
// The token on the wire (three dot-separated Base64URL parts):
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzM2NTAwMDAwLCJleHAiOjE3MzY1MDM2MDB9.3xq9...signature
// 1) header -> describes the signing algorithm
{
"alg": "HS256",
"typ": "JWT"
}
// 2) payload -> the claims (registered + custom). NOT secret!
{
"sub": "user_42", // subject: the user id
"role": "admin", // custom claim
"iss": "api.devatlas", // issuer
"iat": 1736500000, // issued at (unix seconds)
"exp": 1736503600 // expires 1 hour later
}The header and payload of a decoded JWT. Both are just Base64URL-encoded JSON — readable by anyone.
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET; // long, high-entropy, never in source
// Sign a token. exp is set via expiresIn; iat is added automatically.
const token = jwt.sign(
{ sub: 'user_42', role: 'admin' }, // custom + subject claims
SECRET,
{ algorithm: 'HS256', expiresIn: '1h', issuer: 'api.devatlas' },
);
console.log(token); // eyJhbGci...Signing a JWT in Node with the jsonwebtoken library.
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET;
try {
// Pinning algorithms prevents the alg:none / algorithm-confusion attack.
const payload = jwt.verify(token, SECRET, {
algorithms: ['HS256'], // never trust the token's own alg header blindly
issuer: 'api.devatlas',
});
// Signature + exp + iss are valid; we can trust the claims.
console.log(payload.sub, payload.role); // 'user_42' 'admin'
} catch (err) {
// TokenExpiredError, JsonWebTokenError (bad signature), etc.
console.error('Invalid token:', err.message);
}Verifying a JWT safely: pin the algorithm and check the issuer.
Common mistakes
Mistake
Storing sensitive data (passwords, full card numbers, secrets) in the JWT payload.
Do this instead
The payload is only Base64URL-encoded, so anyone can read it. Put only non-secret claims (ids, roles, expiry) in it.
Mistake
Trusting the alg field from the token and accepting alg:none or a weak secret.
Do this instead
Pin the expected algorithm server-side, reject "none", and use a long, high-entropy secret (or asymmetric keys).
Interview questions
Quiz
Flashcards
Show answer · Space