Difficulty: Intermediate
How does JWT authentication work in a Node.js API? Implement a complete auth flow with access and refresh tokens.
JWT (JSON Web Token) is a stateless authentication mechanism. The server creates a signed token containing user data (payload), sends it to the client, and the client includes it in subsequent requests via the Authorization header.
A JWT has three parts: header (algorithm, type), payload (user data, expiration), and signature (HMAC or RSA). The server verifies the signature on each request without hitting the database, making it very efficient.
The recommended pattern uses short-lived access tokens (15 min) and long-lived refresh tokens (7 days). When the access token expires, the client uses the refresh token to get a new one without re-entering credentials.
JWTs should be stored in httpOnly cookies (not localStorage) to prevent XSS attacks. The refresh token should be stored in the database so it can be revoked. Never store sensitive data in the JWT payload since it is base64-encoded, not encrypted.
const jwt = require('jsonwebtoken');
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
// Generate tokens
function generateTokens(user) {
const accessToken = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ id: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Verify token
function verifyAccessToken(token) {
try {
return jwt.verify(token, ACCESS_SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
throw new Error('Invalid token');
}
}
Access tokens are short-lived with user info. Refresh tokens are long-lived with minimal data. Always use environment variables for secrets.
const jwt = require('jsonwebtoken');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
req.user = decoded; // { id, email, role }
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Role-based authorization
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.get('/api/admin/users',
authenticate,
authorize('admin'),
adminController.getUsers
);
The auth middleware extracts the token from the Authorization header, verifies it, and attaches the decoded user to req. The authorize middleware checks roles.
const bcrypt = require('bcrypt');
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const { accessToken, refreshToken } = generateTokens(user);
// Store refresh token in DB (for revocation)
await db.refreshTokens.create({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
// Set refresh token as httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken, user: { id: user.id, email: user.email } });
});
// Refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.cookies;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
const stored = await db.refreshTokens.findByToken(refreshToken);
if (!stored) return res.status(401).json({ error: 'Invalid refresh token' });
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const user = await db.users.findById(decoded.id);
const { accessToken } = generateTokens(user);
res.json({ accessToken });
});
Login returns both tokens. Refresh token is stored in DB and httpOnly cookie. The refresh endpoint issues new access tokens without re-authentication.
JWT, jsonwebtoken, Bearer Token, Access Token, Refresh Token