Authentication Failures: Brute Force, Credential Stuffing and MFA

Intermediate
12 min

Authentication Failures: Brute Force, Credential Stuffing and MFA

The login page is the most attacked endpoint of any application, and the attacks are cheap: billions of leaked email and password pairs are freely traded, and a script can try them against your site in hours. OWASP's "Identification and Authentication Failures" (A07) covers everything that lets those attempts succeed. The password lesson covered hashing; this one covers the flow around it.

In this lesson you will learn how to slow attackers down without locking out real users, prevent account enumeration, add TOTP-based MFA in Node.js, and design a password reset that is not the weakest link.

What Goes Wrong

  • Credential stuffing: leaked passwords from another site are replayed against yours; it works because people reuse passwords.
  • Brute force: many guesses against one account, or one common password against many accounts ("spraying").
  • Enumeration: "no such user" versus "wrong password" reveals which emails are registered.
  • Weak recovery and missing MFA: guessable reset tokens, security questions, no second factor on accounts that can move money.

Slow Down the Attacker

Rate limiting is the first control, and it must be keyed on more than the IP, because stuffing attacks come from thousands of addresses:

javascript
const rateLimit = require("express-rate-limit"); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 10, // per key per window keyGenerator: (req) => `${req.ip}:${String(req.body.email || "").toLowerCase()}`, standardHeaders: "draft-7", legacyHeaders: false }); app.post("/login", loginLimiter, loginHandler);

Add a second limiter per account regardless of IP, and escalate: after a few failures require a CAPTCHA or email confirmation rather than a hard lockout, which attackers abuse to lock real users out. Log every failure with account and IP.

Prevent Enumeration

The sample code returns the same message and takes the same time whether the email exists or not, because it always verifies a hash (a dummy one for unknown users). Apply the same rule wherever identity is checked:

| Endpoint | Leaky behaviour | Safe behaviour | |---|---|---| | Login | "User not found" | "Invalid email or password" | | Password reset | "No account for that email" | "If an account exists, we sent a link" |

Status codes must match too; a 404 for unknown users is as revealing as a message.

Adding Multi-Factor Authentication

Time-based one-time passwords (TOTP) are the most widely supported second factor. Enrolment generates a secret, shows it as a QR code, and verifies one code before enabling it:

javascript
const { authenticator } = require("otplib"); const secret = authenticator.generateSecret(); // store encrypted, per user const uri = authenticator.keyuri(user.email, "ShopExample", secret); // render as a QR code // After the user scans it and types the first code: if (authenticator.check(req.body.code, secret)) { user.totpSecret = secret; // enable MFA user.backupCodes = generateHashedBackupCodes(8); // one-time recovery codes }

Rate limit code verification (a six-digit code has only a million possibilities), issue hashed single-use backup codes, and require re-authentication for sensitive actions such as disabling MFA ("step-up"). Where you can, offer passkeys (WebAuthn): the credential is bound to your origin, so it cannot be phished.

Password Reset Done Right

The reset flow is a second login path and deserves the same care:

javascript
const token = crypto.randomBytes(32).toString("hex"); // emailed to the registered address only await ResetToken.create({ userId: user.id, hash: sha256(token), expiresAt: Date.now() + 15 * 60_000 });

Store only the hash, expire the token after 15 minutes, delete it after one use, and end all sessions when the password changes. Never use security questions; their answers are public.

Quick Quiz
Question 1 of 2

Why does the login handler verify a dummy hash when the email does not exist?

Key Takeaways

  • Credential stuffing and brute force succeed without rate limiting, MFA and unique passwords.
  • Rate limit by IP and by account, escalate with CAPTCHA or email confirmation, avoid hard lockouts.
  • Use identical messages, status codes and timing for known and unknown users everywhere.
  • Implement TOTP with otplib, protect it with rate limits and backup codes, and prefer passkeys.
  • Reset tokens are random, hashed, short-lived and single-use; a password change ends every session.

Next lesson: JWT Pitfalls and Token Security — what goes wrong when sessions become self-contained tokens.

Authentication Failures: Brute Force, Credential Stuffing and MFA - Cyber Security | CodeYourCraft | CodeYourCraft