Your login form works on the first try. A password manager autofills, the token comes back, the user lands in the dashboard. What the model didn't build is everything that happens when the login form is called a million times, or when the token is never rotated, or when the password reset link never expires.
Broken Authentication is the whole family of bugs where proving who you are goes wrong: brute-forceable login, guessable tokens, forever-valid JWTs, password reset links that never expire, sessions that don't terminate. Any one of them lets an attacker become a user they are not.
What your AI actually built
You asked for auth. You got a /login route that checks a password, signs a JWT, and sends it back. It works. That's the part that gets tested.
What it didn't build: rate limiting on login. Lockout after failed attempts. Short-lived tokens. Rotation on password change. A refresh flow that actually invalidates old tokens. A reset link that expires in fifteen minutes instead of forever.
On top of that, half the generated JWT middleware accepts tokens with `alg: none`, or signs with a hardcoded secret that ends up in the client bundle. The login passes testing because nobody tested the parts that matter.
How it gets exploited
The attacker finds the login endpoint from a mobile app or SPA bundle — /api/auth/login, JSON in, JWT out.
- 1Dump a wordlistThey grab the top 10,000 passwords from a breach corpus and run them against a list of emails scraped from a public leak. No rate limiting, so all 10,000 requests per account go through.
- 2Crack a tokenThey sign up legitimately, grab their JWT, and check the algorithm. HS256 with a six-character secret copied from a Stack Overflow answer. Offline dictionary attack finds it in under a minute.
- 3Forge anyoneNow they can mint a token for any user_id they want. The server happily accepts it because the signing key is the same for every user and the expiration is two weeks out.
- 4PersistEven after the real user changes their password, the attacker's forged token still validates. Nothing rotates the key, nothing tracks token version, nothing logs out the other session.
The attacker has a permanent backdoor into every account on the platform — not because they broke crypto, but because the auth system was built as a shape, not as a lifecycle.
Vulnerable vs Fixed
// app/api/auth/login/route.ts
import jwt from 'jsonwebtoken';
const JWT_SECRET = 'supersecret'; // hardcoded, short, copied from a tutorial
export async function POST(req) {
const { email, password } = await req.json();
const user = await db.user.findUnique({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 });
}
// 30-day token. No jti. No refresh. No revocation.
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '30d' });
return Response.json({ token });
}// app/api/auth/login/route.ts
import jwt from 'jsonwebtoken';
import { rateLimit } from '~/lib/rate-limit';
const JWT_SECRET = process.env.JWT_SECRET!; // 256-bit, server-side only
export async function POST(req) {
const ip = req.headers.get('x-forwarded-for') ?? 'unknown';
const { email, password } = await req.json();
const ok = await rateLimit(`login:${ip}:${email}`, { max: 5, window: '15m' });
if (!ok) return Response.json({ error: 'Too many attempts' }, { status: 429 });
const user = await db.user.findUnique({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 });
}
const token = jwt.sign(
{ sub: user.id, tokenVersion: user.tokenVersion },
JWT_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' },
);
return Response.json({ token });
}Three cheap changes: a per-IP-per-email rate limit stops brute force, a real secret out of env stops forgery, and a token version in the claims lets you bump it on password change and invalidate every old session at once. The refresh flow (not shown) lives behind a cookie with stricter controls.
A real case
Peloton's API let strangers pull any user's private profile data
In 2021 a researcher found that Peloton's API returned profile data for any user ID to unauthenticated callers — a missing auth check that turned 3 million users into an open directory.
Related reading
What we find
broken authenticationReferences
Stress test your auth the way an attacker would.
Flowpatrol probes login, reset, refresh, and JWT handling across every endpoint. Five minutes, one URL, no agent.
Try it free