What you'll learn
Password login combines slow password derivation, enumeration-resistant responses, an opaque server-side session, and a hardened cookie. Build the lifecycle as one security boundary.
By the end of this lesson, you'll be able to:
- Hash passwords with async scrypt and random salts
- Verify without timing leaks
- Issue and rotate opaque sessions
- Harden cookies and logout
Core mental model
Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.
| Concept | What it means | Decision rule |
|---|---|---|
| Password KDF | A deliberately expensive one-way derivation | Use a reviewed async KDF and tune its cost |
| Session ID | A random bearer credential referencing server state | Store only an opaque high-entropy value in the cookie |
| Rotation | Replacing a credential after privilege change | Regenerate on login, elevation, reset, and suspected compromise |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Define the session authentication boundary: inputs, outputs, invariants, ownership, and expected failures.
- Design the data or message contract before choosing implementation details.
- Implement the smallest correct path with dependencies passed explicitly.
- Add validation, failure translation, cleanup, and concurrency behavior.
- Verify the boundary with realistic data and at least one adversarial case.
- Measure or observe the behavior before optimizing or extracting abstractions.
Keep the feedback loop short
Guided code lab
Derive a password hash asynchronously
A random 16-byte salt defeats precomputation; timingSafeEqual protects the comparison after lengths match.
import { randomBytes, scrypt as callbackScrypt, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
const scrypt = promisify(callbackScrypt);
export async function hashPassword(password) {
const salt = randomBytes(16);
const hash = await scrypt(password, salt, 64);
return { salt: salt.toString('base64'), hash: hash.toString('base64') };
}
export async function verifyPassword(password, record) {
const expected = Buffer.from(record.hash, 'base64');
const actual = await scrypt(password, Buffer.from(record.salt, 'base64'), expected.length);
return timingSafeEqual(expected, actual);
}Production practice
Contract
Login returns one generic failure, rotates the session on success, and sets Secure, HttpOnly, SameSite, Path, and expiry explicitly.
Verification
Test valid/invalid users with indistinguishable responses, session fixation, cookie flags, expiry, logout, and password-change revocation.
Operations
Rate-limit by account and network, tune KDF cost, encrypt session storage, and expose revocation and suspicious-login events.
Common failure mode
Independent workshop
Add registration, login, current-session, and logout endpoints.
Your finished workshop must include:
- Validated credentials
- Async password KDF
- Generic login failure
- Server-side session store
- Hardened cookie
- Rotation/revocation tests
Definition of done
Recap & quick check
Key takeaways
- Passwords use slow KDFs
- Salts are random per password
- Sessions are bearer credentials
- Cookies need explicit flags
- Privilege changes rotate sessions
Quick check
1. Why use a password KDF?
2. What belongs in the cookie?
3. When regenerate a session?
Next: JWTs, Access Tokens & Refresh Rotation