JWT authentication has become the default way to secure modern APIs, single-page apps, and microservices. But behind its popularity hides a fair amount of confusion: How is a token actually built? Is it really safer than sessions? What happens when it expires? In this guide, we break down everything you need to know about JSON Web Tokens, with real code examples and the pitfalls we see most often in production.
What is JWT Authentication?
A JSON Web Token (JWT) is a compact, URL-safe string that securely represents claims between two parties. In an authentication context, the server issues a JWT after a user logs in successfully. The client then sends this token with every subsequent request, and the server verifies it without needing to look up a session in a database.
This makes JWT authentication stateless, which is why it pairs so well with REST APIs, mobile apps, and distributed systems.

The Anatomy of a JWT: Header, Payload, Signature
A JWT looks like three Base64-encoded strings separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkphbmUiLCJleHAiOjE3NDk4ODAwMDB9.s5K3Z1Xq...signature
1. Header
Describes the token type and the signing algorithm used.
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload
Contains the claims: information about the user and metadata about the token itself.
{
"sub": "1234",
"name": "Jane Doe",
"role": "admin",
"iat": 1749876400,
"exp": 1749880000
}
Common standard claims include:
- sub : subject (user ID)
- iat : issued at
- exp : expiration time
- iss : issuer
- aud : audience
Important: the payload is encoded, not encrypted. Anyone can decode it. Never put passwords or sensitive secrets in there.
3. Signature
The signature ensures the token has not been tampered with. It is computed like this:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
If the payload is modified, the signature no longer matches, and the server rejects the token.
How the JWT Authentication Flow Works
Here is the typical exchange between a client and a server:
- The user submits credentials (email + password) to
/login. - The server validates them and generates a signed JWT.
- The client stores the token (memory, httpOnly cookie, or secure storage).
- For every protected request, the client sends the token in the
Authorization: Bearer <token>header. - The server verifies the signature and the expiration, then processes the request.

JWT vs Session-Based Authentication
Choosing between JWT and traditional sessions is one of the most common architecture decisions. Here is a clear comparison:
| Criteria | JWT (Stateless) | Sessions (Stateful) |
|---|---|---|
| Storage | Client-side | Server-side store (DB, Redis) |
| Scalability | Excellent across services | Requires shared session store |
| Revocation | Hard (needs blocklist) | Easy (delete session) |
| Performance | No DB hit per request | Lookup on every request |
| Best for | APIs, microservices, mobile | Classic web apps |
Practical Example: Issuing and Validating a JWT in Node.js
Issuing a token at login
const jwt = require('jsonwebtoken');
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', issuer: 'devchatt.org' }
);
res.json({ access_token: token });
});
Validating a token in middleware
function authMiddleware(req, res, next) {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = header.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'devchatt.org',
algorithms: ['HS256']
});
req.user = payload;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
Handling Token Expiration the Right Way
Short-lived tokens are safer, but they create a UX problem: users get kicked out. The standard solution is the access token + refresh token pattern:
- Access token: short lifespan (5 to 15 minutes), sent on every request.
- Refresh token: long lifespan (days or weeks), stored securely (httpOnly cookie), used only to get a new access token.
When the access token expires, the client silently calls /refresh to obtain a new one. The refresh token can be revoked server-side, giving you back the control you lose with pure JWT.

Common JWT Pitfalls to Avoid
- Storing tokens in localStorage: vulnerable to XSS. Prefer httpOnly cookies for the refresh token.
- Accepting the
nonealgorithm: always whitelist algorithms during verification. - Putting sensitive data in the payload: it is just Base64, not encrypted.
- Using long expiration times: a stolen token with a 30-day expiry is a disaster.
- Forgetting to validate
issandaud: these protect against tokens issued by another system. - No revocation strategy: implement a blocklist for critical actions like logout-everywhere.
- Weak secrets: use at least 256 bits of entropy, or switch to RS256 with key pairs.
When Should You Use JWT Authentication?
Good fit:
- REST or GraphQL APIs consumed by SPAs or mobile apps
- Microservices that need to pass identity across boundaries
- Service-to-service authentication
- Stateless serverless functions
Probably not the best fit:
- Traditional server-rendered web apps where sessions work perfectly
- Apps that need immediate, granular revocation of credentials
- Small monolithic projects where Redis sessions are simpler
FAQ: JWT Authentication
How does JWT authentication work in simple terms?
The server gives the client a signed token after login. The client sends that token with every request, and the server checks the signature to confirm the user is authenticated, without storing anything server-side.
What is the difference between JWT and OAuth?
OAuth 2.0 is a framework for authorization (how an app gets permission to access resources). JWT is a token format. OAuth often uses JWTs as its access tokens, but they solve different problems.
Is JWT the same as JSON?
No. JWT uses JSON for its header and payload, but the final token is a Base64-encoded, signed string. JSON is just the data format used inside.
Are JWTs still used in 2026?
Yes, heavily. They remain the standard for API authentication, OpenID Connect, and microservice communication. The best practices have just matured around shorter lifespans and refresh tokens.
Where should I store a JWT on the client?
Store short-lived access tokens in memory, and refresh tokens in httpOnly, Secure, SameSite cookies. Avoid localStorage for anything sensitive due to XSS risk.
Can a JWT be revoked before it expires?
Not natively, since the server does not track them. You need a revocation list (often in Redis) or you rely on short expiration plus refresh token revocation.
Final Thoughts
JWT authentication is powerful, but it is not magic. Used correctly, with short-lived tokens, refresh rotation, strong secrets, and proper claim validation, it gives you a scalable and clean authentication layer. Used carelessly, it becomes a security liability. Take the time to design the flow before writing the first jwt.sign(), and your future self will thank you.
