How JWT Authentication Works: A Developer's Guide
JWTs are used in virtually every modern authentication system — OAuth 2.0, OpenID Connect, API keys. This guide explains their structure, how signing and verification work, the security trade-offs, and when sessions are a better choice.
The Structure of a JWT
A JWT is three Base64url-encoded JSON objects joined by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c3JfMTIzIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzQzMDMzNjAwLCJleHAiOjE3NDMxMjAwMDB9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└── Header ──┘ └───────────────── Payload ──────────────────────┘ └─── Signature ───┘Header
Specifies the token type and signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}Payload
Contains claims — statements about the user and token metadata:
{
"sub": "usr_123", // Subject: user identifier
"name": "Alice",
"iat": 1743033600, // Issued At (Unix timestamp)
"exp": 1743120000, // Expiration (Unix timestamp)
"role": "admin"
}Standard claim names are defined by the JWT spec: iss (issuer), sub (subject), aud (audience), exp (expiration), nbf (not before), iat (issued at), jti (JWT ID).
Signature
Proves the token hasn't been tampered with:
HMACSHA256(
base64url(header) + "." + base64url(payload),
secret
)The signature covers both the header and payload. If either is modified, the signature becomes invalid. The server can detect any tampering.
The Authentication Flow
- Login: User sends credentials (username + password) to
POST /auth/login. - Token issuance: Server verifies credentials, creates a JWT signed with its secret key, returns it to the client.
- Authenticated requests: Client includes the JWT in the
Authorizationheader:Authorization: Bearer <token>. - Verification: Server receives the token, verifies the signature using its secret key, checks the
expclaim, and trusts the claims in the payload.
// Step 4 — server-side verification (Node.js)
const jwt = require('jsonwebtoken');
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}Why JWTs Are Stateless
Traditional session-based auth stores session data server-side (in memory or a database). The client holds an opaque session ID. On every request, the server looks up the session ID to get user data.
With JWTs, all user information lives in the token itself. The server doesn't need to look anything up — it just verifies the signature and reads the claims. This makes JWTs ideal for:
- Microservices — each service can verify tokens independently without calling an auth service
- Horizontal scaling — no shared session store needed across instances
- Cross-domain authentication — tokens work across different domains; cookies don't
HS256 vs RS256 vs ES256
The algorithm choice determines how signing and verification work:
- HS256 (HMAC-SHA256) — symmetric. One shared secret for signing and verifying. Simple, but every verifier can also issue tokens.
- RS256 (RSA-SHA256) — asymmetric. Private key signs, public key verifies. Services can verify without being able to issue tokens.
- ES256 (ECDSA-SHA256) — asymmetric like RS256 but with smaller key sizes and faster operations. Preferred for new systems.
Use HS256 for simple single-service setups. Use RS256 or ES256 when multiple services need to verify tokens (publish your public key as a JWKS endpoint).
Common Security Mistakes
1. Accepting "alg: none"
Early JWT libraries had a vulnerability where setting "alg": "none" in the header bypassed signature verification entirely. Always explicitly specify the accepted algorithm(s) when verifying:
// Vulnerable
jwt.verify(token, secret);
// Safe — reject unexpected algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });2. Not validating exp
Most libraries validate exp automatically, but always confirm yours does. A token without expiry validation is valid forever after theft.
3. Putting sensitive data in the payload
The payload is Base64url-encoded, not encrypted. Anyone with the token can read it. Only include claims that are safe to expose: user ID, roles, permissions. Never put passwords, PII, or secrets.
4. Storing JWTs in localStorage
Any XSS vulnerability on your site can read localStorage and steal all tokens. Prefer HttpOnly cookies — JavaScript can't access them.
JWT vs Sessions: When to Use Each
| Concern | JWT | Session |
|---|---|---|
| Instant revocation | Hard (need denylist) | Easy (delete session) |
| Horizontal scaling | Easy (stateless) | Needs shared store |
| Microservices | Great | Complex |
| Payload size | Grows with claims | Fixed (opaque ID) |
| Server storage | None | Required |
For most web apps with a single backend: sessions are simpler and safer by default. For APIs, microservices, or mobile apps: JWTs are the pragmatic choice. Many systems use both — JWTs for stateless API auth, sessions for web browser authentication.
Inspect a JWT right now with the JWT Decoder — paste any token to view its header, payload claims, and expiry status. 100% client-side, your token never leaves the browser.
Frequently Asked Questions
Are JWTs encrypted?▾
By default, no. A standard JWT (JWS — JSON Web Signature) is signed but not encrypted. The header and payload are Base64url-encoded, which anyone can decode instantly. They are not secret. The signature only proves that the token was issued by someone who holds the secret key and hasn't been tampered with. If you need to hide the payload contents, use JWE (JSON Web Encryption), a different spec that encrypts the payload.
Can I invalidate a JWT before it expires?▾
Not directly — this is the biggest drawback of stateless JWTs. Because the server doesn't store session state, it can't 'forget' a token. Common workarounds: (1) Keep a server-side blocklist/denylist of invalidated token JTI (JWT ID) claims. (2) Use short expiry times (15 minutes) and refresh tokens. (3) Change the signing secret to invalidate all tokens at once (nuclear option). Each approach trades some statefulness back into the system.
Where should I store a JWT on the client?▾
localStorage is convenient but vulnerable to XSS — any JavaScript running on your page can read it. An HttpOnly cookie is safer against XSS because JavaScript can't access it, but it's vulnerable to CSRF (mitigated with SameSite=Strict or a CSRF token). For most applications, HttpOnly, Secure, SameSite=Strict cookies are the safer default. Avoid localStorage for tokens that grant access to sensitive operations.
What is the difference between HS256 and RS256?▾
HS256 (HMAC-SHA256) uses a single shared secret for both signing and verification. Everyone who can verify tokens can also create them. RS256 (RSA-SHA256) uses a private key to sign and a public key to verify. The server keeps the private key secret; any service can verify tokens using the public key without being able to issue new ones. Use RS256 in microservices architectures where multiple services need to verify tokens but shouldn't be able to issue them.
Should I put sensitive data in the JWT payload?▾
No. JWT payloads are readable by anyone who has the token — they're Base64url-encoded, not encrypted. Only put non-sensitive claims in the payload: user ID, roles, expiry time. Never put passwords, PII, payment details, or secrets in a JWT payload. If you need to attach sensitive data to a session, use opaque session tokens with server-side storage instead.