Crux Read real auth snippets — Argon2id parameters, a naive SHA-256 store, a timing-unsafe compare, and a missing rehash-on-login — and pick the fix a senior would make first.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
Password bugs hide in code that looks fine and passes its happy-path test. Read each snippet, find the defect a breach or a load test would expose, and choose the fix a senior engineer reaches for first.
Goal
Practise the review loop: read an auth path, locate the cryptographic or timing defect, and pick the highest-leverage correction before shipping it to production.
What is wrong with this, and what is the single highest-leverage fix?
Heads-up Salting defeats rainbow tables but leaves SHA-256's speed untouched — the leaked column is still cracked in hours. You must change the algorithm to a slow KDF, not just bolt on a salt.
Heads-up SHA-512 is also a fast general-purpose hash; longer output does not slow down guessing. The fix is a deliberately slow, memory-hard KDF, not a bigger fast hash.
Heads-up Encoding is cosmetic and changes nothing about crackability. The problem is the algorithm choice and the missing salt.
The algorithm choice is right. What's wrong with the tuning?
Heads-up argon2id is exactly the right variant — it blends data-independent and data-dependent passes for both GPU and side-channel resistance. The bug is the under-sized memory and time costs, not the variant.
Heads-up p=1 matches the OWASP recommendation; parallelism is a throughput knob, not the security lever. The real defect is the 512 KiB memory cost.
Heads-up 512 KiB is roughly 1/38th of OWASP's 19 MiB minimum, so a GPU can run many guesses in parallel again. These parameters are dangerously weak, not conservative.
Snippet 3 — the comparison
function verifyResetToken(provided, stored) { // both are hex-encoded HMAC digests if (provided === stored) { return true; } return false;}
Quiz
Completed
This compares two secret digests with ===. Why is that a problem and what's the fix?
Heads-up String === in V8 short-circuits on the first mismatching character, so its duration depends on how many leading characters matched — exactly the leak. Use a constant-time comparison.
Heads-up Returning a boolean is correct API design. The defect is the timing variability of ===, not the return type.
Heads-up Re-hashing doesn't make the final comparison constant-time, and comparing the re-hashes with === has the same leak. Use a timing-safe equality primitive on the bytes.
Snippet 4 — login without migration
async function login(email, password) { const user = await db.users.findByEmail(email); if (!user) return null; const ok = await argon2.verify(user.passwordHash, password); if (!ok) return null; return issueSession(user);}
Quiz
Completed
The verify is correct and constant-time. What does this login miss that matters as parameters age?
Heads-up Hardware speeds up, so 2020 parameters are weak today. Login is the one moment you hold the plaintext and can transparently rehash, which is exactly when the migration should happen.
Heads-up Re-hashing every login wastes CPU and serves no purpose when parameters already match. You rehash only when needsRehash reports the stored params are outdated.
Heads-up A forced reset is unnecessary and user-hostile. You already have the plaintext during a successful login, so you can rehash silently with no reset.
Recap
These are the defects you find in real auth reviews: a fast hash with no salt must become a slow, memory-hard KDF; Argon2id with under-sized memory cost throws away the property that defeats GPUs, so honor OWASP’s m=19 MiB/t=2/p=1; secret comparisons must be constant-time (crypto.timingSafeEqual, or the KDF’s own verify) rather than ===; and a correct verify still needs rehash-on-login via needsRehash to keep the work factor current without forced resets. Diagnose the algorithm, the parameters, the timing, and the migration — in that order.