The idea: proving who you are, and keeping it proved
Authentication is how an app decides who is sending a request. On the web that has two halves: the login (a password, maybe a second factor) and the session that follows (a random id in a cookie, which stands in for the password on every later request). A07:2025 Authentication Failures is item 7 of the OWASP Top 10: any weakness in either half that lets an attacker act as another user. It slipped from second place in 2017 to seventh, mostly because frameworks now ship good session handling, but credential stuffing keeps it on the list.
OWASP maps the category to dozens of CWEs. The ones this page shows:
- CWE-287 Improper Authentication (the umbrella) and CWE-1390 Weak Authentication;
- CWE-307 Improper Restriction of Excessive Authentication Attempts (stuffing, brute force);
- CWE-521 Weak Password Requirements;
- CWE-204 Observable Response Discrepancy and CWE-208 Observable Timing Discrepancy (user enumeration);
- CWE-384 Session Fixation;
- CWE-613 Insufficient Session Expiration (logout that doesn't log out);
- CWE-340/CWE-565 predictable tokens and cookies trusted without a check (remember-me).
Reading the canvas: the client side holds alice's browser and mallory's attack script (its list of requests and what each one got back). The server side holds the login endpoint, a list of the fixed app's seven defenses, the users table, the session store and the remember_tokens table. Blue boxes are alice's requests, pink ones mallory's, grey ones SQL, green and red ones replies. The clock adds one second per request. Each demo runs the attack against the vulnerable app, then against the fixed app; the defense that stopped it turns orange.
Credential stuffing
Billions of user/password pairs from breached sites are public. Many people reuse passwords, so an attacker takes a breached list from one site and replays it at another. It needs no guessing at all: alice's real password is on the list (Demo 1: credential stuffing). In the vulnerable app the third pair logs mallory in as alice.
The fixed app stops it twice. The per-IP rate limit (3 attempts per 60 s) answers the fourth attempt with 429 Too Many Requests. But real stuffing comes from thousands of proxy IPs, so the decisive defense is multi-factor authentication: the right password is no longer enough without a code from alice's phone. A breached-password check (comparing new passwords against known leaks, for example with the k-anonymity range API of Have I Been Pwned) would have refused Summer2024! in the first place.
Brute force and backoff
A brute-force attack guesses one account's password. With no limit a script tries thousands of guesses a minute, and a weak password such as bob1990 falls quickly (Demo 2: brute force vs backoff). The guesses come from four botnet IPs, so a per-IP limit never fires: the limit must also be per account.
The fixed app counts consecutive failures in users.failed. After 3 it locks the account for 2 s, then 4, 8, 16 … (exponential backoff), and while it is locked it answers 429 without even checking the password, so the right guess at t = 4 s is wasted. bob, typing his password wrong twice, loses a few seconds; an attacker loses years.
A hard lockout ("locked until an admin unlocks it") is worse: anyone can lock anyone out by failing three times. Short, growing locks, a CAPTCHA after a few failures, and alerts on many failures across accounts are the usual mix. Weak password rules (CWE-521) make all of this easier; NIST SP 800-63B asks for at least 8 characters (15 when the password is the only factor), a check against breached and common passwords, and no forced composition rules or periodic changes.
User enumeration
Before guessing, an attacker wants to know which accounts exist. If the login says "No such user" for one name and "Wrong password" for another, it tells them (Demo 3: user enumeration). Even with one message, timing leaks it: the vulnerable app answers an unknown name in 2 ms because there is no hash to check, and a known name in 81 ms (one bcrypt compare).
The fix is the same reply for every failure, "Invalid user or password", and the same work: hash the submitted password against a dummy hash when the user does not exist. The same applies to the sign-up form ("this email is already registered") and the password-reset form ("we sent a link if the account exists").
Session fixation
Many apps give every visitor a session before login. If the login keeps that id and just marks it as logged in, an attacker who got their own id into the victim's browser shares the victim's session afterwards (Demo 4: session fixation): mallory takes an anonymous id, plants it in alice's cookie jar (a cookie set from a sibling subdomain, or an app that accepts ?sid= in the URL), waits for alice to log in, and then uses the same id.
The fix is one line in most frameworks: issue a new session id at every login (and every privilege change) and delete the old one, as the fixed app does. Then a planted id is worthless. Never accept session ids from URLs, and use the __Host- cookie prefix so a subdomain can't set the cookie (see How HTTP Cookies Work).
Logout and session lifetime
A session id is as good as the password for as long as the server accepts it. If logout only tells the browser to delete the cookie (Set-Cookie: sid=; Max-Age=0), any copy of the id, from a shared computer, a proxy log or malware, keeps working (Demo 5: logout that doesn't log out). The fix is to delete the session on the server at logout, and to give every session an idle timeout (15 minutes here) and an absolute timeout (8 hours), whatever the cookie says.
Self-contained tokens such as JWTs have the same problem in a harder form: the server keeps no row to delete, so a stolen token works until it expires. Keep them short-lived, pair them with a refresh token that can be revoked, or keep a deny list (see How OAuth 2.0 Works).
Remember-me tokens
A "remember me" cookie logs the browser in again when the session is gone, so it is a long-lived password. The vulnerable app stores base64("user=mallory") in it: base64 is an encoding, not a protection, so mallory decodes it, writes user=alice and is alice (Demo 6: weak remember-me token). The fixed app sets 128 random bits, keeps only their SHA-256 in remember_tokens and looks the hash up; an edited token matches nothing. Production schemes split it into a selector (to find the row) and a validator (compared in constant time), rotate it on every use, and delete all of a user's tokens when the password changes.
How to prevent it
- Offer or require MFA; prefer phishing-resistant factors (passkeys, WebAuthn: see How Passkeys Sign You In) over SMS codes.
- Check new passwords against breached and common password lists; follow NIST SP 800-63B length rules; no default or hard-coded credentials (CWE-798).
- Limit attempts per account (growing backoff) and per IP, alert on many failures across accounts, and log every failure (see A09).
- Return the same message and timing for every login, sign-up and reset failure.
- Store passwords with a slow, salted hash: Argon2id, scrypt or bcrypt (see A04).
- Rotate the session id at login; make ids long and random; send them only in
Secure; HttpOnly; SameSitecookies, never in URLs. - Invalidate sessions on the server at logout and on password change; enforce idle and absolute timeouts.
- Make remember-me and password-reset tokens random, single-purpose, stored hashed, and expire them.
- Use the framework's well-tested authentication and session code instead of writing your own.
OWASP Top 10:2025 series
- A01 Broken Access Control
- A02 Security Misconfiguration
- A03 Software Supply Chain Failures
- A04 Cryptographic Failures
- A05 Injection
- A06 Insecure Design
- A07 Authentication Failures (this page)
- A08 Software or Data Integrity Failures
- A09 Security Logging and Alerting Failures
- A10 Mishandling of Exceptional Conditions
See also How HTTP Cookies Work for cookie attributes and How OAuth 2.0 Works for delegated login and token lifetimes.
What the page leaves out
Real session ids and tokens are 128 random bits, not four characters; real bcrypt hashes are 60 characters. The per-IP window never slides because every scenario fits in 60 s. The page leaves out password-reset flows (CWE-640), phishing and real-time phishing proxies that relay MFA codes, MFA fatigue, SIM swapping, CAPTCHAs, device fingerprinting and risk-based login, passkeys and WebAuthn, single sign-on and federated login, account recovery, and how the defenses themselves can leak (a 429 on a locked account tells the attacker the account exists).