In A01 we distinguished authentication ("who are you?") from authorization ("what can you do?") and said authentication would get its own lesson. Here it is. A07:2021 – Identification and Authentication Failures —previously called "Broken Authentication"— groups everything that goes wrong when verifying a user's identity and maintaining that identity throughout the session: weak passwords, brute force and credential stuffing, poor session management, absence of MFA, insecure account recovery.

It helps to place it among its neighbors: A07 validates who you are (login, sessions, MFA); A01 decides what you can do once identified; and A02 deals with how credentials are stored (bcrypt/argon2). The three collaborate, but solve different problems. In BazarNube we'll review the login and session management of the Express API, two of the most attacked points of any application with user accounts.

Legal and ethical notice: the attack examples (brute force, credential stuffing) are illustrative and use fake data. Practice only on systems you own or have explicit authorization to test.

Contents

  1. Weak passwords and modern policies
  2. Brute force and credential stuffing
  3. Error messages and user enumeration
  4. Secure session management
  5. Multi-factor authentication (MFA)
  6. Account recovery without holes
  7. Common mistakes, exercises and solutions

  1. Weak passwords and modern policies

BazarNube's registration accepted any password, including the most common ones:

// register.js — VULNERABLE: no password policy
router.post('/api/register', async (req, res) => {
  const { email, password } = req.body;
  await createUser(email, password);   // accepts "123456", "password"...
  res.json({ ok: true });
});

Current recommendations (aligned with NIST and the OWASP Authentication Cheat Sheet) have shifted from the old rules:

  • Reasonable minimum length (e.g. 12) and allow long passwords (support at least 64 characters, without truncating).
  • Check against lists of breached/common passwords (reject "password", "bazarnube2024"...).
  • Don't impose absurd composition rules (mandatory uppercase + symbol) or forced periodic expiration: they encourage predictable patterns. Better: length + exposure check.
  • Allow password managers (don't block pasting).
// register.js — SECURE: modern policy
const zxcvbn = require('zxcvbn');               // estimates real strength
router.post('/api/register', async (req, res) => {
  const { email, password } = req.body;
  if (password.length < 12) return res.status(400).json({ error: 'Minimum 12 characters' });
  if (zxcvbn(password).score < 3) return res.status(400).json({ error: 'Weak password' });
  if (await isBreached(password)) return res.status(400).json({ error: 'Breached password' });
  await createUser(email, password);            // stored with bcrypt/argon2 (see A02)
  res.json({ ok: true });
});

isBreached checks (in a privacy-preserving way, using k-anonymity) whether the password appears in known breaches. Remember: how the password is stored we already solved in A02 with bcrypt/argon2.

  1. Brute force and credential stuffing

  • Brute force: trying many passwords against one account.
  • Credential stuffing: using username/password pairs leaked from other breaches, betting that people reuse credentials. It's today the most common attack against logins.

BazarNube's login had no brake at all:

// login.js — VULNERABLE: no attempt limit
router.post('/api/login', async (req, res) => {
  const u = await getUser(req.body.email);
  if (u && await verifyPassword(req.body.password, u.pwd)) {
    req.session.userId = u.id;
    return res.json({ ok: true });
  }
  res.status(401).json({ error: 'Invalid credentials' });
});

With no limits, an attacker fires thousands of attempts per minute. Combined defenses:

// login.js — SECURE: rate limiting + progressive lockout
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({ windowMs: 15*60*1000, max: 10 }); // per IP

router.post('/api/login', loginLimiter, async (req, res) => {
  const email = req.body.email;
  if (await isLockedOut(email)) return res.status(429).json({ error: 'Too many attempts' });
  const u = await getUser(email);
  if (u && await verifyPassword(req.body.password, u.pwd)) {
    await resetFailures(email);
    await regenerateSession(req);           // see section 4 (session fixation)
    req.session.userId = u.id;
    return res.json({ ok: true });
  }
  await recordFailure(email);               // progressive lockout per account
  res.status(401).json({ error: 'Invalid credentials' });
});
Control Protects against
Rate limiting per IP Brute force from one source
Progressive lockout per account Attacks targeting one user
CAPTCHA after N failures Mass automation
MFA Credential stuffing (even if they guess the password)
Anomalous login detection Distributed credential stuffing

Credential stuffing uses many IPs and a single password per account, so a per-IP limit isn't enough: that's why MFA and pattern detection are key.

  1. Error messages and user enumeration

A subtle detail: if the login responds "this email doesn't exist" versus "incorrect password", you hand the attacker a way to enumerate which accounts exist. The same goes for differing response times.

// VULNERABLE: reveals whether the email exists
if (!u) return res.status(404).json({ error: 'User not found' });
if (!ok) return res.status(401).json({ error: 'Incorrect password' });

// SECURE: uniform message and behavior
// (same response whether the user exists or not; same time cost)
return res.status(401).json({ error: 'Invalid credentials' });

The same care applies to registration ("this email is already registered") and to password recovery (section 6): always respond uniformly.

  1. Secure session management

Once authenticated, the session is what "remembers" who you are. Its flaws are as serious as the login's.

flowchart LR
  A[Successful login] --> B[Regenerate session id]
  B --> C[HttpOnly Secure SameSite cookie]
  C --> D[Idle and absolute expiration]
  D --> E[Logout invalidates session on the server]

Good practices:

  • Regenerate the session id on login (regenerateSession): prevents session fixation, where the attacker fixes a known id before login and inherits it afterward.
  • Cookies with HttpOnly (not accessible by JS, mitigates XSS —see 03-04), Secure (HTTPS only) and SameSite (mitigates CSRF).
  • Expiration: by inactivity (e.g. 30 min) and absolute (e.g. 12 h), after which re-authentication is required.
  • Real logout: invalidate the session on the server, not just delete the cookie on the client.
  • Session tokens random and with enough entropy (the framework generates them; don't invent your own).
// secure session config (Express)
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false, saveUninitialized: false,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30*60*1000 }
}));

function regenerateSession(req) {
  return new Promise((ok, err) => req.session.regenerate(e => e ? err(e) : ok()));
}

If you use JWT instead of server sessions, mind their own set of risks: verified signature (fixed algorithm, never alg: none), short expiration, and a revocation mechanism (JWTs don't invalidate themselves on logout).

  1. Multi-factor authentication (MFA)

MFA adds a second factor (something you have, like a TOTP or a passkey) to the "something you know" (the password). It's the most effective defense against credential stuffing: even if the attacker has the correct password, without the second factor they don't get in.

  • Offer MFA to all BazarNube customers and require it for admin accounts and sensitive operations (payment changes, exports).
  • Prefer TOTP (authenticator apps) or WebAuthn/passkeys (phishing-resistant) over SMS, which is more vulnerable to SIM swapping.
  • Consider step-up authentication: prompt for the second factor only on critical actions, not on every login.

  1. Account recovery without holes

The "I forgot my password" flow is a classic weak point: if it's insecure, the attacker uses it to take over the account without knowing the password.

  • Generate a single-use random token with a short expiration (e.g. 15-30 min), store only its hash, and send it over a verified channel (email).
  • Don't reveal whether the email exists: always respond "if the account exists, we've sent instructions".
  • Invalidate the token after using it and after changing the password; close active sessions when it's reset.
  • Never use guessable security questions ("your pet's name") as the sole recovery factor.
// recovery — secure pattern (summary)
router.post('/api/forgot', async (req, res) => {
  const u = await getUser(req.body.email);
  if (u) {
    const token = crypto.randomBytes(32).toString('hex');
    await saveResetToken(u.id, sha256(token), Date.now() + 30*60*1000); // store hash + expiration
    await sendEmail(u.email, resetLink(token));
  }
  res.json({ message: 'If the account exists, we will send instructions' }); // uniform response
});

Common Mistakes and Tips

  • Login with no limits. Rate limiting + progressive lockout are essential.
  • Messages that reveal whether a user exists. Make responses and timings uniform (login, registration, recovery).
  • Not regenerating the session on login. Opens the door to session fixation.
  • Cookies without HttpOnly/Secure/SameSite. Enables session theft and CSRF.
  • Not offering MFA or leaving it optional on admin accounts.
  • Recovery with long, non-expiring or guessable tokens. A direct path to account takeover.
  • Confusing A07 with A02. A07 validates and maintains identity; A02 stores the credential (bcrypt/argon2).
  • Tip: rely on proven solutions (session frameworks, identity providers) before building authentication from scratch.

Exercises

Exercise 1. Explain why per-IP rate limiting alone doesn't stop a credential stuffing attack and which controls complement it.

Exercise 2. This login leaks information. Identify the problem and fix it:

const u = await getUser(email);
if (!u) return res.status(404).json({ error: 'That email is not registered' });
if (!await verify(pass, u.pwd)) return res.status(401).json({ error: 'Wrong password' });

Exercise 3. What is session fixation and which single, well-placed line prevents it?

Solutions

Solution 1. Credential stuffing spreads across many IPs and tries one password (the leaked one) per account, so it barely triggers per-IP limits. It's complemented by: MFA (even if they guess the password, the second factor is missing), anomalous pattern detection (many distinct accounts, suspicious agents), breached-password checking at registration, and CAPTCHA/step-up on signs of automation.

Solution 2. It allows user enumeration: it distinguishes "email not registered" (404) from "wrong password" (401). Fix: a uniform response, same status code and message whether the user exists or not, and taking care that the response time doesn't give the case away (return res.status(401).json({ error: 'Invalid credentials' }) in both).

Solution 3. Session fixation happens when the attacker gets the victim to use a session id the attacker already knows (e.g. fixed via URL) and, after login, inherits that authenticated session. It's prevented by regenerating the session id right at authentication (req.session.regenerate(...)), so the previous id becomes useless.

Conclusion

A07 protects the front door and the user's continued presence: modern password policies (length + exposure check), brakes on brute force and credential stuffing (rate limiting, lockout, MFA), uniform messages against enumeration, solid session management (regeneration, secure cookies, expiration, real logout) and account recovery with single-use ephemeral tokens. All resting on the secure storage we already set up in A02.

Backlog entry — A07: password policy with minimum length and breached-password checking; rate limiting + progressive lockout on login; uniform login/registration messages; session regeneration and HttpOnly/Secure/SameSite cookies; MFA mandatory for admin; recovery flow with a hashed ephemeral token.

We now know how to identify and authenticate. But can we trust the integrity of the software and data flowing through BazarNube —updates, serialized objects, pipeline artifacts? The next lesson tackles A08:2021 – Software and Data Integrity Failures, including insecure deserialization in the Java module.

OWASP Course: Guidelines and Standards for Web Application Security

Module 1: Introduction to OWASP

Module 2: Main OWASP Projects

Module 3: OWASP Top Ten 2021 in Depth

Module 4: OWASP ASVS (Application Security Verification Standard)

Module 5: OWASP SAMM (Software Assurance Maturity Model)

Module 6: OWASP ZAP (Zed Attack Proxy)

Module 7: Best Practices and Recommendations

Module 8: Practical Exercises and Case Studies

Module 9: Assessment and Certification

© Copyright 2026. All rights reserved