Broken Authentication
Harden login, token, session, and password mechanisms so attackers cannot impersonate legitimate users.
URL: trusteed.io/academy/api-security/broken-authentication
Broken Authentication covers weaknesses in the mechanisms that verify a user's identity — login flows, token issuance and validation, password handling, session management, and multi-factor authentication. Because authentication code is written once and rarely revisited, and because AI assistants often generate the "simplest working version" of a JWT or login handler, this category is unusually prone to silent, long-lived flaws.
What Is Broken Authentication?
Authentication is broken whenever an attacker can impersonate a legitimate user without knowing their actual credentials — through weak password policies, unthrottled login attempts (enabling credential stuffing or brute force), tokens that don't expire or can't be revoked, JWTs with insufficient signature verification, or session identifiers that are predictable or never invalidated on logout. Unlike BOLA, which is about authorization after identity is established, Broken Authentication is about identity establishment itself failing.
Why AI Coding Assistants Get This Wrong
JWT and login-handler code is a common request pattern, and the fastest correct-looking implementation frequently skips steps that don't affect whether the demo "works": no rate limiting on the login endpoint, no algorithm confusion protection when verifying JWTs, tokens that never expire (convenient for testing, dangerous in production), and passwords hashed with fast, GPU-crackable algorithms instead of purpose-built slow hashes. None of these omissions break the happy path a quick test would exercise — they only matter under attack.
Vulnerable Pattern
// JWT verification that accepts the "none" algorithm and never checks expiration
const jwt = require('jsonwebtoken');
function verifyToken(token) {
return jwt.decode(token); // decode() does NOT verify signature or expiration
}
// Login endpoint with no rate limiting and fast password hashing
app.post('/api/login', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (user && md5(req.body.password) === user.passwordHash) {
const token = jwt.sign({ userId: user.id }, SECRET); // no expiresIn
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
Secure Pattern
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');
// Enforce signature + expiration verification explicitly
function verifyToken(token) {
return jwt.verify(token, SECRET, { algorithms: ['HS256'] }); // pin algorithm
}
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes per IP
message: 'Too many login attempts, try again later',
});
app.post('/api/login', loginLimiter, async (req, res) => {
const user = await User.findOne({ email: req.body.email });
const valid = user && await bcrypt.compare(req.body.password, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: 'Invalid credentials' }); // same message either way
}
const token = jwt.sign({ userId: user.id }, SECRET, {
algorithm: 'HS256',
expiresIn: '15m', // short-lived access token
});
const refreshToken = issueRefreshToken(user.id); // separate, revocable, longer-lived
res.json({ token, refreshToken });
});
Note the identical error message for "user not found" and "wrong password" — this prevents user enumeration through differing error responses, a subtler but real Broken Authentication risk.
Copy-Paste Rules for Cursor, Claude Code, and Codex
---
description: Broken authentication prevention — tokens, passwords, and login flows
alwaysApply: true
---
# Authentication Security Rules
- Never use `jwt.decode()` for authorization decisions — it does not verify the
signature. Always use `jwt.verify()` with an explicit, pinned algorithm list.
- Never accept the "none" algorithm or allow the algorithm to be read from the
token itself. Pin the expected algorithm(s) in the verification call.
- Every issued access token MUST have a short expiration (minutes, not days).
Use a separate, revocable refresh token for long-lived sessions.
- Hash passwords only with bcrypt, scrypt, or argon2 — never md5, sha1, or sha256
alone. Use a cost factor appropriate to current hardware (bcrypt rounds >= 12).
- Every authentication endpoint (login, password reset, OTP verification, token
refresh) MUST have rate limiting applied before writing any other logic.
- Return identical error messages and response timing for "user not found" and
"wrong password" to prevent user enumeration.
- Never log passwords, tokens, or full credential payloads, even in debug mode.
- Support and default to requiring MFA for privileged accounts and sensitive
operations when the application has an MFA capability.
- On logout or password change, invalidate existing sessions/refresh tokens —
do not rely on access token expiry alone.
- When generating a login or token-handling endpoint, explicitly implement
rate limiting, algorithm pinning, and password hashing in the same pass —
do not treat them as optional follow-ups.
Verification Checklist
- JWT verification pins the algorithm and checks expiration on every protected route
- Login, registration, password reset, and OTP endpoints are rate-limited
- Passwords are hashed with bcrypt/argon2/scrypt at an appropriate cost factor
- Access tokens are short-lived; refresh tokens are separately revocable
- Sessions/tokens are invalidated on logout and password change
- Error responses do not reveal whether an email/username exists in the system
- MFA is available and enforced for privileged roles
- Continuous testing validates authentication endpoints under attack conditions — see Trusteed's WAAP platform for bot and credential-stuffing defense at runtime
Frequently asked questions
Is a long, random API key enough authentication for machine-to-machine APIs?
A sufficiently long, randomly generated key is a reasonable baseline for service-to-service auth, but it still needs rate limiting, rotation capability, and revocation — a leaked static key with no expiration or rotation path is a long-lived Broken Authentication risk.
Why is rate limiting an authentication issue and not just a resource issue?
Unthrottled login endpoints enable credential stuffing and brute-force attacks directly against the authentication mechanism itself — the "resource" being protected is the account, not just server capacity, which is why it's classified under Broken Authentication specifically, distinct from API4's general resource consumption concerns.
Should refresh tokens be stored in localStorage or cookies?
HttpOnly, Secure, SameSite cookies are generally preferred over localStorage for refresh tokens, since localStorage is readable by any JavaScript running on the page, including injected via XSS — a cookie-based refresh token limits that exposure.