Skip to content
back to the map
technique
stage: code
3 min read

A07 Authentication Failures

Login is the front door. Unlimited guesses, reused passwords, sessions that never expire and tokens that are not really verified all let an attacker walk in as a real user.

Hydra / ffuf (on systems you own)Have I Been Pwned Passwords APIjwt.io / jwt_toolPasskeys / WebAuthnOWASP ZAPKeycloak / Auth0 / Cognito

What it is

Authentication failures are weaknesses in confirming a user's identity or managing the session that follows: permitting and brute force, accepting weak or breached passwords, exposing session ids, skipping or mis-verifying tokens, or offering a weak recovery flow.

:2025 lists it as A07 under the name Authentication Failures.

How the attack works

  • Credential stuffing: lists of leaked e-mail and password pairs from other sites are replayed against your login at scale.
  • Brute force and password spraying: a few common passwords across many accounts, below any naive lockout.
  • Weak JWT handling: the server accepts alg: none, never checks the signature or the expiry, or uses a guessable secret.
  • and theft: the session id is not rotated after login, travels in the URL, or the cookie lacks HttpOnly and Secure.
  • Account recovery bypass: reset links that never expire or security questions with public answers.

Vulnerable code

No throttling, a fast comparison of an unsalted hash, a session id kept across login, and a token that is decoded but never verified.

login.js (Express)javascript
app.post("/login", async (req, res) => {
  const user = await db.user.findByEmail(req.body.email);
  if (user && user.passwordHash === sha1(req.body.password)) {
    req.session.userId = user.id;          // same session id as before login
    return res.json({ ok: true });
  }
  res.status(401).json({ error: "wrong email or password" });   // unlimited attempts
});

const claims = jwt.decode(token);          // decode() does NOT verify the signature
tokens.pypython
claims = jwt.decode(token, options={"verify_signature": False})

The fix

Throttle by account and by IP, verify passwords with a slow hash, regenerate the session on login, and set cookie flags. Verify tokens with a pinned algorithm, a real key, and issuer, audience and expiry checks. Where you can, use passkeys or an identity provider instead of building login yourself, and add a second factor for sensitive accounts.

login.js (Express)javascript
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 10, keyGenerator: (r) => `${r.ip}:${r.body?.email}` });

app.post("/login", limiter, async (req, res) => {
  const user = await db.user.findByEmail(req.body.email);
  const ok = user && (await argon2.verify(user.passwordHash, req.body.password));
  if (!ok) return res.status(401).json({ error: "wrong email or password" });   // same message for both cases
  req.session.regenerate(() => {         // new id after login: defeats fixation
    req.session.userId = user.id;
    res.json({ ok: true });
  });
});

app.use(session({ cookie: { httpOnly: true, secure: true, sameSite: "lax", maxAge: 3600_000 } }));
tokens.py (PyJWT)python
claims = jwt.decode(
    token,
    key=PUBLIC_KEY,
    algorithms=["RS256"],          # pin the algorithm; never accept "none"
    audience="api.example.com",
    issuer="https://id.example.com",
    options={"require": ["exp", "iat", "sub"]},
)
SecurityConfig.java (Spring Security)java
http.sessionManagement(s -> s.sessionFixation().migrateSession())
    .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));   // verifies signature, exp, issuer via config

Prove the fix

Test the behaviors an attacker relies on: the eleventh attempt is refused, the session id changes after login, and tampered or unsigned tokens are rejected.

test_auth.py (pytest)python
def test_login_is_throttled(client):
    codes = [client.post("/login", json={"email": "a@x.io", "password": f"guess{i}"}).status_code for i in range(15)]
    assert 429 in codes

def test_session_id_changes_after_login(client, user):
    before = client.get("/").cookies.get("sid")
    client.post("/login", json=user.credentials)
    assert client.cookies.get("sid") != before

def test_unsigned_token_is_rejected(client):
    forged = b64(b'{"alg":"none"}') + "." + b64(b'{"sub":"admin"}') + "."
    assert client.get("/api/me", headers={"Authorization": f"Bearer {forged}"}).status_code == 401

def test_cookie_flags(client, user):
    cookie = client.post("/login", json=user.credentials).headers["set-cookie"]
    assert "HttpOnly" in cookie and "Secure" in cookie

Checklist

  • Rate limit and monitor login by account, by IP and by device; add progressive delay or challenge, not just hard lockout (which enables denial of service).
  • Check new passwords against a breached-password list; require length over arbitrary complexity rules.
  • Offer multi-factor authentication, ideally phishing-resistant (passkeys, ), and require it for admins.
  • Rotate the session id on login and privilege change; expire it; invalidate it on logout.
  • Verify every token: signature, algorithm, issuer, audience, expiry. Never trust decode alone.
  • Make recovery as strong as login: single-use, short-lived links; same generic response for known and unknown e-mails.
Credential stuffingAttack that tests leaked username/password pairs from other services at scale. Signature: many login failures spread across many IPs, with few attempts per account.see in glossary →OWASP Top 10The best-known reference list of web application security risks (A01 broken access control, A02 cryptographic failures, A03 injection...), revised every few years by the OWASP community. It is an educational map of risk categories, not a tool checklist or a certification.see in glossary →Session fixationAn attacker plants a known session id in the victim's browser; if the app keeps that id after login, the attacker shares the session. Regenerate the id on login.see in glossary →PasskeyWebAuthnA phishing-resistant login based on a key pair stored on the user's device; the site holds only the public key, so there is no password to steal or stuff.see in glossary →

connections on the map

updated on 14/10/2026 · sources to go deeper · report an error

in other domains

  • Bot managementsecopscredential stuffing is, first of all, automated traffic

Unlocks

test what you learned

Loading questions…