Handbooks / Node.js / Chapter 8
Authentication and Security Basics
7 pages · ~5 min✓ Reviewed
Builds on Databases from Node. Next up: Testing Node Apps.
Node Security: Mental Model and Layers
1. Defense in Depth
- No single control is enough: stack the layers
- Hashing, auth, headers, throttling, validation, secrets, audits
- Never trust client input: body, query, params, headers, cookies are hostile until validated
| Threat | Control |
|---|---|
| Stolen DB | Password hashing |
| XSS / clickjacking | helmet headers |
| Brute force / DoS | Rate limiting |
| Injection / bad data | zod validation |
| Leaked keys | Env secrets |
| Vulnerable deps | npm audit |
2. Middleware Order
- helmetheaders
- Rate limiterthrottle
- Body parserread JSON
- Validationzod
- Authwho are you
- Handlerroute logic
Password Hashing with bcrypt and argon2
1. Hash, never encrypt
- One-way hash: a stolen DB reveals no passwords
- Never store plaintext or fast hashes: MD5, SHA-256
- Salt is random per password, stored inside the hash string
- Salt defeats rainbow tables; both libraries add it
2. bcrypt vs argon2
const h = await bcrypt.hash(pw, 12) await bcrypt.compare(pw, h) const a = await argon2.hash(pw) await argon2.verify(a, pw)
bcrypt: cost 12, each +1 doubles time, input cut at 72 bytes
Sessions vs JWT
1. Session vs JWT
- Session: server stores state (memory, Redis, DB)
- Cookie holds an opaque session ID; logout deletes it
- JWT: signed
header.payload.signaturewith claims (sub,exp) - Stateless: server only verifies the signature
- Payload is encoded, not encrypted: no secrets
helmet, Rate Limiting and Zod Validation
1. helmet and CSP
app.use(helmet())sets security headers in one line- Adds CSP, HSTS,
X-Content-Type-Options: nosniff, X-Frame-Options - Removes the X-Powered-By header
- CSP limits where scripts, styles and images load from: the main XSS mitigation
app.use(helmet.contentSecurityPolicy({
directives: { defaultSrc: ["'self'"] }
}));tune CSP directives
Secrets in Environment Variables and npm audit
1. Where secrets live
- DB URL, JWT secret, API keys → env vars, never in Git
- Read with
process.env.JWT_SECRET - Dev:
.envvia dotenv ornode --env-file=.env .gitignorethe.env; commit only.env.example- Prod: inject from the platform, not baked into images
- Use different secrets per environment
Node Security Cheat Sheet
1. Hashing and Auth
- argon2id or bcrypt cost 12+, async only
- Check with
compare/verify, never=== - Never plaintext or SHA-256
- Same generic error for bad email or password
- Cookies: httpOnly + secure + sameSite=lax
- JWT: short expiry, pin the algorithm
Check yourself
Login does if (hash === await bcrypt.hash(pw, 12)). Valid passwords are always rejected. Why?
- bcrypt adds a random salt each call, so the same password gives a different hash
- Use
await bcrypt.compare(pw, hash); it reads the salt from the stored hash
The server reads the user id with jwt.decode(token).sub. What can an attacker do?
decodeskips the signature check, so a forged token with anysubis accepted