Critical Mistakes When Implementing Web Authentication
Explore common and dangerous mistakes in auth implementations: weak hashing, user enumeration, broken token resets, and production security fixes.
In software engineering, Authentication is notoriously deceptive. It is deceptively easy to get functioning—a junior developer can write basic registration and login flows in an afternoon—but extraordinarily difficult to make bulletproof and secure.
Many catastrophic data breaches across the industry did not originate from elite zero-day exploits, but rather from fundamental mistakes embedded right into custom authentication systems.
1. Explain Like I’m 10 (ELI5): The Steel Vault and the Doormat#
Imagine building a high-security house:
- You spend thousands of dollars installing a massive 8-inch steel vault door at the front entrance (Enforcing 16-character complex passwords).
- But you leave the spare house key under the front doormat (Storing JWT tokens in
localStoragevulnerable to XSS). - You leave the ground floor back window unlocked (Omitting login rate limits and brute-force throttling).
- And when a stranger rings the doorbell asking “Does Bob live here?”, you honestly answer: “Yes, Bob lives here, but he’s asleep” (User enumeration vulnerability confirming account existence).
No matter how thick your front door is, an intruder walks in without breaking a sweat.
flowchart TD
subgraph Vulnerable["Vulnerable Auth Pipeline"]
A1["Wrong password entered"] --> B1["Error: 'User exists, wrong password' - Leaks User"]
A2["Brute Force Attempt"] --> B2["No Rate Limiting - Password cracked"]
A3["Password Reset"] --> B3["Token never expires + Sessions remain active"]
end
subgraph Secure["Hardened Auth Pipeline"]
C1["Wrong password entered"] --> D1["Generic: 'Invalid credentials'"]
C2["5 Failed Attempts"] --> D2["15-min IP Throttling + CAPTCHA"]
C3["Password Reset"] --> D3["Secure Token TTL 15m + All sessions revoked"]
end
2. The 7 Most Dangerous Authentication Mistakes#
Mistake 1: Hashing Passwords with Fast Algorithms (MD5, SHA-1, Vanilla SHA-256)#
A widespread misconception among developers is that passing a password through SHA-256 makes it secure. This is critically wrong.
- The Problem: General-purpose algorithms like
SHA-256andMD5are designed to be extremely fast, calculating billions of hashes per second. - The Risk: In the event of a database leak, an attacker using modern consumer GPUs can calculate tens of billions of SHA-256 hashes per second to reverse passwords via rainbow tables and brute-force dictionaries.
[!TIP] The Fix: Always use dedicated memory-hard, slow password hashing algorithms:
- Argon2id (The #1 OWASP recommended algorithm).
- bcrypt (with a
work factor >= 12).- scrypt or PBKDF2.
import argon2 from 'argon2';
// Hash password with industry-standard Argon2id
export async function hashPassword(password: string): Promise<string> {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB RAM
timeCost: 3, // 3 iterations
parallelism: 4
});
}
// Timing-safe password verification
export async function verifyPassword(hash: string, plain: string): Promise<boolean> {
return await argon2.verify(hash, plain);
}typescriptMistake 2: Leaking Account Existence (User Enumeration)#
When an authentication request fails, systems frequently reveal too much detail:
- ❌ “Email not found in our database.”
- ❌ “Incorrect password for user@example.com.”
An attacker can automate a credential-checking script against your endpoint with a list of 100,000 corporate emails. Your API neatly categorizes which people have accounts, setting the stage for targeted spear-phishing and password spraying.
[!NOTE] The Fix: Return uniform, neutral responses:
- Failed Login: “Invalid email or password.”
- Password Reset: “If an account exists with that email, a password reset link has been dispatched.”
Mistake 3: Missing Rate Limiting and Timing Attack Protections#
Without request rate limiting:
- Attackers run endless credential stuffing attacks against user accounts.
- Automated bots exhaust server CPU resources on password verification routines.
Furthermore, naive string comparisons (===) can leak secrets via Timing Attacks (where the server responds micro-seconds faster when leading characters match):
import crypto from 'crypto';
// ❌ BAD: Leaks string length and character matches via execution time
// if (userProvidedToken === serverSecret) ...
// ✅ GOOD: Constant-time comparison
export function safeEqual(a: string, b: string): boolean {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return crypto.timingSafeEqual(bufA, bufB);
}typescriptMistake 4: Flawed Password Reset Workflows#
Password reset is the single most targeted component of authentication:
- Weak or Predictable Tokens: Generating 4-digit codes or using pseudo-random generators (
Math.random()) instead of cryptographic entropy (crypto.randomBytes(32)). - Infinite Token Lifetimes: Reset links that never expire.
- Failing to Invalidate Active Sessions: After changing a password, existing session cookies and refresh tokens remain valid on stolen devices!
[!WARNING] Upon any password reset or password change, you MUST immediately invalidate all active Sessions and Refresh Tokens across all connected devices!
Mistake 5: Confusing Authentication with Authorization (IDOR)#
- Authentication (401 Unauthorized): Verifies who you are (e.g., User A).
- Authorization (403 Forbidden): Verifies what you are allowed to access.
Developers often assume that once a user is authenticated, route parameters can be trusted. This causes critical Insecure Direct Object Reference (IDOR) vulnerabilities:
// ❌ IDOR VULNERABILITY: Any authenticated user can read other users' invoices!
app.get('/api/invoices/:id', authenticateUser, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
// Missing Check: if (invoice.userId !== req.user.id) return res.status(403);
return res.json(invoice);
});typescriptMistake 6: Insecure Client Token Storage (LocalStorage vs. Cookies)#
- Storing Tokens in
localStorage: Vulnerable to Cross-Site Scripting (XSS). Any rogue third-party npm package or injected script can extract all user tokens instantly. - Storing in Cookies without Protection Flags: Forgetting to apply
HttpOnly,Secure, andSameSite=Lax/Strictflags, leaving apps exposed to CSRF.
Mistake 7: “Rolling Your Own Cryptography”#
Writing bespoke cryptographic scrambling functions or homebrewed auth handshakes under the belief that “hackers cannot break what they cannot see” is known as Security through Obscurity—a strategy guaranteed to fail.
Established protocols like OAuth 2.0, OpenID Connect, and WebAuthn have undergone decades of rigorous academic and industry scrutiny.
3. Production Authentication Pre-Flight Checklist#
| Security Requirement | Status | Recommendation |
|---|---|---|
| Password Storage | 🟩 | Argon2id or bcrypt (cost factor >= 12) |
| Rate Limiting | 🟩 | Max 5 failed attempts per IP/Account per minute |
| Error Messages | 🟩 | Opaque responses; eliminate user enumeration |
| Reset Tokens | 🟩 | 256-bit cryptographically secure, single-use, TTL < 15m |
| Password Change | 🟩 | Revoke all existing active sessions and refresh tokens |
| Cookie Flags | 🟩 | Mandatory HttpOnly, Secure, and SameSite=Lax |
| Object Authorization | 🟩 | Explicitly verify resource.ownerId === req.user.id |
4. Summary#
A resilient authentication system does not require forcing users into arbitrary 30-character password rules. It relies on architectural rigor, defensive programming, and adherence to proven cryptographic standards.
Audit your auth pipelines against these common vulnerabilities before deploying to production.