Working with Password-Based Authentication
1. Implementing Username-Password Login
| Step | Best Practice |
| Transport | HTTPS only, HSTS enabled |
| Lookup | Case-insensitive username, constant-time |
| Verify | bcrypt/argon2 compare (never plain ==) |
| Response | Generic error to prevent enumeration |
| Post-login | Rotate session ID, set Secure cookie |
Example: Java Spring Boot login
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
User user = userRepo.findByEmailIgnoreCase(req.email())
.orElseThrow(() -> new BadCredentialsException("Invalid credentials"));
if (!passwordEncoder.matches(req.password(), user.passwordHash())) {
throw new BadCredentialsException("Invalid credentials");
}
String token = jwtService.issue(user);
return ResponseEntity.ok(new TokenResponse(token));
}
2. Hashing Passwords
| Algorithm | Recommended Params (2026) | Notes |
| Argon2id PREFERRED | m=64MB, t=3, p=1 | OWASP recommended winner |
| bcrypt | cost=12 (min) | Widely supported, 72-byte limit |
| scrypt | N=2^17, r=8, p=1 | Memory-hard |
| PBKDF2-HMAC-SHA256 | ≥600,000 iterations | FIPS compliant |
| MD5 / SHA1 / SHA256 DEPRECATED | — | Fast = crackable |
Example: Argon2id in Node.js
import argon2 from "argon2";
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 1
});
const ok = await argon2.verify(hash, password);
3. Salting Passwords
| Property | Requirement |
| Uniqueness | Per-user, never reused |
| Length | ≥16 bytes (128 bits) |
| Source | CSPRNG (crypto.randomBytes) |
| Storage | Embedded in hash string (bcrypt/argon2 do this) |
| Pepper | App-wide secret added to password (kept in HSM/vault) |
4. Setting Password Complexity Requirements
| Requirement (NIST SP 800-63B) | Recommendation |
| Minimum length | ≥8 chars (12+ preferred) |
| Maximum length | ≥64 chars allowed |
| Allowed chars | All Unicode + spaces |
| Composition rules | NOT required (no forced symbols) |
| Breach check | Reject if in HIBP/breach list |
| Periodic change | NOT required unless compromise |
Warning: NIST 2017+ guidance deprecated forced complexity rules and periodic resets — they encourage weaker passwords.
5. Implementing Password Strength Validation
Example: zxcvbn entropy check
import zxcvbn from "zxcvbn";
const result = zxcvbn(password, [email, username]);
if (result.score < 3) {
throw new Error(`Weak password: ${result.feedback.suggestions.join(" ")}`);
}
// score: 0 (too guessable) → 4 (very strong)
| Score | Crack Time | Verdict |
| 0 | < 10² guesses | Reject |
| 1 | 10⁶ | Reject |
| 2 | 10⁸ | Weak |
| 3 | 10¹⁰ | Acceptable |
| 4 | ≥10¹⁰ | Strong |
6. Preventing Password Enumeration Attacks
| Surface | Mitigation |
| Login | Generic "Invalid email or password" |
| Registration | Send confirmation email regardless of existence |
| Password reset | Always respond "If account exists, email sent" |
| Timing | Always compute hash even for unknown users |
| Rate limiting | Per IP + per identifier |
7. Implementing Account Lockout Policies
| Parameter | Typical Value |
| Threshold | 5–10 failed attempts |
| Window | 15 minutes |
| Lockout duration | 15–30 min (exponential backoff) |
| Scope | Per account + per IP |
| Unlock | Timer expiry or email link |
8. Handling Password Reset Flow
Password Reset Process
- User requests reset (POST /forgot-password with email)
- Server generates cryptographically-random token (32 bytes)
- Hash token, store with userId + expiry (15 min)
- Email plaintext token in single-use link
- User clicks link → verifies token hash + expiry
- User submits new password → hash + store, invalidate token
- Invalidate all existing sessions for the user
- Send confirmation email
9. Implementing Secure Password Recovery
| Control | Implementation |
| Token entropy | ≥128 bits (CSPRNG) |
| Storage | SHA-256 hashed in DB |
| Expiration | ≤15 minutes |
| Single use | Delete on first use |
| Transport | HTTPS link only |
| Session invalidation | Revoke all sessions after reset |
10. Using Password Breach Detection
Example: HIBP k-anonymity check
import { createHash } from "crypto";
async function isPwned(password) {
const sha1 = createHash("sha1").update(password).digest("hex").toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const body = await res.text();
return body.split("\n").some(line => line.startsWith(suffix));
}
| Service | Use |
| HaveIBeenPwned | K-anonymous SHA-1 prefix API (free) |
| Enzoic | Commercial breach + dark-web monitoring |
| SpyCloud | Enterprise credential exposure |
11. Enforcing Password Expiration Policies
| Trigger | Action |
| Suspected compromise | Force reset immediately |
| Breach detected | Invalidate + email user |
| Regulatory | Periodic only if mandated (PCI DSS 4.0: optional with MFA) |
| High-privilege accounts | 90-day rotation if no MFA |
12. Preventing Password Reuse
| Strategy | Detail |
| History table | Store last N=5–10 password hashes |
| Comparison | Verify against each historical hash (Argon2 verify) |
| Cross-site | Block known-breached passwords (HIBP) |
| Similarity | Reject Levenshtein distance < 3 from previous |