JWT vs Session Authentication: Choosing the Right Approach


Authentication is a critical aspect of web application security. Two popular approaches are JWT (JSON Web Tokens) and Session-based authentication. Each has its strengths and weaknesses. In this post, we'll explore both methods and help you choose the right one for your application.
Session authentication stores user state on the server. When a user logs in, the server creates a session and sends a session ID (usually in a cookie) to the client.
// Login endpoint
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Verify credentials
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session
req.session.userId = user.id;
req.session.save();
res.json({ message: 'Login successful' });
});
// Protected route
app.get('/profile', requireAuth, (req, res) => {
// req.session.userId is available
res.json({ user: req.user });
});
JWT authentication is stateless. The server creates a token containing user information, signs it, and sends it to the client. The client includes this token in subsequent requests.
import jwt from 'jsonwebtoken';
// Login endpoint
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ token });
});
// Protected route
app.get('/profile', authenticateToken, async (req, res) => {
// req.user is set by authenticateToken middleware
const user = await User.findById(req.user.userId);
res.json({ user });
});
// Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
}
A common pattern is using both: short-lived JWT access tokens with refresh tokens.
// Login
app.post('/login', async (req, res) => {
const user = await validateUser(req.body);
const accessToken = jwt.sign(
{ userId: user.id },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in database
await RefreshToken.create({
userId: user.id,
token: refreshToken
});
res.json({ accessToken, refreshToken });
});
// Refresh access token
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
const storedToken = await RefreshToken.findOne({ token: refreshToken });
if (!storedToken) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
const newAccessToken = jwt.sign(
{ userId: user.userId },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
});
});
// Secure session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // Prevent XSS
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: 'strict' // CSRF protection
}
}));
// Secure JWT implementation
const token = jwt.sign(
payload,
process.env.JWT_SECRET,
{
expiresIn: '15m',
issuer: 'your-app',
audience: 'your-app-users'
}
);
// Always verify
jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'your-app',
audience: 'your-app-users'
});
Both JWT and Session authentication have their place:
Choose based on your specific requirements: architecture, security needs, and scalability concerns. Remember, security is not just about the authentication method—it's about proper implementation and following best practices!