JWT auth without the confusion
JWTs are everywhere, but they're often misunderstood. Let's strip away the jargon and see what they actually are, how they work, and how to use them safely in your apps.
What is a JWT?
A JWT (JSON Web Token) is just a string with three parts separated by dots:
header.payload.signature
- Header: contains the algorithm and token type.
- Payload: contains claims (data like user id, expiration, etc.).
- Signature: used to verify the token hasn't been tampered with.
All parts are base64url encoded. The signature is created by hashing the header and payload with a secret (or private key).
How does authentication work with JWTs?
- User logs in with credentials.
- Server verifies credentials and creates a JWT with user info in the payload.
- Server sends the token back to the client.
- Client stores the token (usually in memory or localStorage) and sends it in the
Authorizationheader for subsequent requests. - Server verifies the token's signature and expiration, then trusts the claims.
That's it. No session storage, no cookies (if you choose), no server-side state.
The classic pitfalls
1. Storing tokens in localStorage
LocalStorage is accessible to any JavaScript running on your page, making it vulnerable to XSS. If an attacker injects script, they can steal the token.
Better: use httpOnly cookies, which are not accessible to JavaScript. But then you need CSRF protection.
2. Not checking expiration
Always check exp claim. Use a library that validates it automatically.
3. Putting sensitive data in the payload
The payload is base64 encoded, not encrypted. Anyone can decode it. Never put passwords, credit card numbers, or other secrets in there.
4. Using a weak secret
If you use HS256 (symmetric), the secret must be long and random. For production, prefer RS256 (asymmetric) with a private/public key pair.
Minimal working example (Node.js + Express)
Here's a simple implementation using jsonwebtoken:
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const SECRET = process.env.JWT_SECRET || 'change-me';
app.use(express.json());
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Check credentials (pseudo)
if (username === 'admin' && password === 'secret') {
const token = jwt.sign({ sub: username }, SECRET, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
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.slice(7);
try {
const payload = jwt.verify(token, SECRET);
req.user = payload;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
app.get('/protected', authMiddleware, (req, res) => {
res.json({ message: 'You are authenticated', user: req.user });
});
app.listen(3000);
This is a minimal but functional flow.
When to use JWT vs sessions
JWT is great for:
- Stateless APIs, especially microservices.
- Mobile apps where cookies are tricky.
- Single sign-on across domains.
Sessions (server-side storage) might be better if:
- You need to revoke tokens instantly (JWTs are valid until they expire unless you maintain a blocklist).
- You have simple, single-server apps.
Security checklist
- Use HTTPS in production.
- Set short expiration times (e.g., 15 minutes) and use refresh tokens.
- Validate the
audandissclaims if using third-party auth. - Keep the secret or private key out of source code.
- Use established libraries, don't roll your own crypto.
Final thoughts
JWT is not magic. It's a signed token that lets you trust the data it carries. Understand what it does and doesn't protect you from, and you'll avoid most common mistakes. Start with a simple flow, then add refresh tokens and secure storage as your app grows.
Happy coding.
Top comments (2)
If an authenticated user is performing malicious activities, how can we immediately revoke that particular user’s access? If the solution is to expire the user’s session or token, they could continue performing malicious actions until the expiration time. How can we ensure their access is revoked immediately?
Immediate revocation and fully stateless JWT validation are mutually exclusive. If access must stop now, each protected request needs a current server-side signal.
A practical setup is short-lived access tokens plus a session record keyed by
sidorjti. On suspension, mark that session revoked or bump a per-usertoken_version; have the API gateway check it from Redis or another fast store before accepting the token. An opaque token with introspection is another option. A denylist works too, but entries must live until the token'sexp.Also revoke refresh tokens and stop active WebSocket, stream, or background work.
expis the backstop, not the immediate kill switch. If you cache the revocation check for 5 seconds, then 5 seconds is your explicit worst-case revocation delay. There isn't a cryptographic trick that gives instant per-user revocation while keeping validation completely stateless.