Overview
Broken Authentication can enable attackers to impersonate users, access sensitive endpoints, and take over accounts. In Express apps, this often happens when tokens or session IDs are not issued, rotated, or invalidated correctly, or when credentials are stored and transmitted insecurely.
In Node.js (Express), vulnerabilities manifest via weak JWT handling, insecure cookie settings, or missing rate limiting. Examples include cookies that are accessible to client-side scripts (HttpOnly missing), non-rotating secrets, and login flows that do not enforce proper authentication checks on protected routes.
Real-world impact spans data exposure, privilege escalation, and broader breach risk when one user token is compromised. Attackers can bypass MFA if present, and maintain long-lasting sessions due to long token lifetimes or ineffective revocation.
Remediation focuses on robust token management, secure cookies, password storage, and lifecycle controls; implement issuer/audience checks, rotate keys, use short-lived tokens, and add rate limiting, logging, and automated tests.
Code Fix Example
Node.js (Express) API Security Remediation
// Vulnerable version
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
app.use(cookieParser());
const SECRET = 'supersecret';
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'admin' && password === 'admin') {
const token = jwt.sign({ sub: username }, SECRET, { expiresIn: '7d' });
res.cookie('token', token, { httpOnly: false, secure: false, sameSite: 'lax' });
return res.json({ ok: true });
}
res.status(401).json({ error: 'Unauthorized' });
});
app.get('/admin', (req, res) => {
const token = req.cookies?.token;
try {
jwt.verify(token, SECRET);
res.json({ secret: '42' });
} catch (e) {
res.status(401).json({ error: 'Unauthorized' });
}
});
app.listen(3000, () => console.log('Vulnerable app listening on port 3000'));
// Fixed version
const fs = require('fs');
const privateKey = fs.readFileSync('./keys/private.pem', 'utf8');
const publicKey = fs.readFileSync('./keys/public.pem', 'utf8');
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');
const express2 = require('express');
const app2 = express2();
app2.use(express2.json());
const { sign, verify } = require('jsonwebtoken');
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, message: 'Too many login attempts, try later.' });
app2.post('/login', loginLimiter, (req, res) => {
const { username, password } = req.body;
// In real apps, fetch user and compare hashed passwords
const hashedPassword = '$2b$10$N9qo8uLOthz18...'; // replace with real hash from DB
if (username === 'admin' && bcrypt.compareSync(password, hashedPassword)) {
const token = sign({ sub: username }, privateKey, { algorithm: 'RS256', expiresIn: '1h', issuer: 'your-app' });
res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 60 * 60 * 1000 });
return res.json({ ok: true });
}
res.status(401).json({ error: 'Unauthorized' });
});
app2.get('/admin', (req, res) => {
const token = req.cookies?.token;
try {
const payload = verify(token, publicKey, { algorithms: ['RS256'], issuer: 'your-app' });
res.json({ secret: '42', user: payload.sub });
} catch (e) {
res.status(401).json({ error: 'Unauthorized' });
}
});
app2.listen(3000, () => console.log('Fixed app listening on port 3000'));