Understanding JWT Authentication
JWTs are everywhere in modern web auth. Here's how they work and how to use them properly.
What is a JWT?
JSON Web Token. A signed string that contains claims (data) you can trust.
Structure: `header.payload.signature`
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.
signature_here
How It Works
1. User logs in with credentials 2. Server verifies credentials 3. Server creates JWT with user data 4. Server sends JWT to client 5. Client stores JWT 6. Client sends JWT with each request 7. Server verifies signature, trusts the data
Creating JWTs
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // Keep this secret!
// Create token
const token = jwt.sign(
{ userId: user.id, email: user.email },
SECRET,
{ expiresIn: '24h' }
);
// Verify token
try {
const decoded = jwt.verify(token, SECRET);
console.log(decoded.userId); // "123"
} catch (err) {
console.log('Invalid token');
}
Express Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = decoded;
next();
});
}
// Usage
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user contains the decoded token data
res.json({ userId: req.user.userId });
});
Storage: Where to Put the Token
Local Storage
localStorage.setItem('token', token);
Pros: Simple Cons: Vulnerable to XSS
HTTP-Only Cookie
res.cookie('token', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict'
});
Pros: Not accessible via JavaScript Cons: Requires CSRF protection
I prefer HTTP-only cookies for sensitive apps.
Common Mistakes
Storing Sensitive Data in Payload
JWTs are encoded, not encrypted. Anyone can decode the payload:
// Don't do this
{ password: "secret123", ssn: "123-45-6789" }
// Do this
{ userId: "123", role: "user" }
Not Setting Expiration
// Always set expiration
jwt.sign(payload, secret, { expiresIn: '24h' });
Using Weak Secrets
// ❌ Bad
const SECRET = "secret";
// ✅ Good
const SECRET = crypto.randomBytes(64).toString('hex');
Not Handling Expiration
// Check if token is expired
jwt.verify(token, SECRET, (err, decoded) => {
if (err?.name === 'TokenExpiredError') {
// Token expired, refresh or re-authenticate
}
});
Refresh Tokens
For better security, use short-lived access tokens with refresh tokens:
1. Access token: 15 minutes 2. Refresh token: 7 days, stored securely 3. When access token expires, use refresh token to get new one
More complex but more secure.