Never Store Passwords
Part of the Auth and Security section of Coddy's Node.js journey. Lesson 4 of 24.
People reuse passwords, so a leaked plaintext password can expose accounts elsewhere. Store a salted password hash: a derived value that you verify by recomputing it, rather than decoding it.
const salt = randomBytes(16).toString('hex');
const key = await scryptAsync(inviteCode, salt, 32);
const saved = salt + '.' + key.toString('hex');This example combines a salt and derived key for an invite code. The delimiter lets verification recover the salt and derive another key with the same parameters. Your password task uses its own delimiter and key length. Generate the salt for each new password, derive the key, then store both. Salted hashes still require strong passwords because an attacker can try guesses.
Challenge
EasyRight now signup saves each password exactly as it was typed. Make it save a salted hash instead, so a leaked database does not give the passwords away.
- In
passwords.js, changehashPassword(password)so it returns the text that signup saves, in the form salt, colon, hash:- The salt is 16 random bytes, made fresh on every call and written as hex: 32 characters from 0 to 9 and a to f.
- The hash is a 64 byte key that
scryptmakes from the password and that salt, also written as hex: 128 characters. The third argument ofscryptAsyncis the key length in bytes. - Join the two with a colon. A saved value looks like this:
f2bba0669d9fa2d203afc7e61bf8f065:c3f7cae352e023a8d4766da9a316b5a2c844100efff22c4079f89a8ab8512ac91c33227481c3afa88c3ba0e88347f9e3e0c22dad9d55f19688a350f71be8dbd2
- You do not need to change
server.js. ItsPOST /signupalready callshashPasswordand saves what it returns. It also hasGET /debug/users/:username, which shows what was saved for one user. That route exists only so you can look; a real app never has it.
Try it: Press Run. Send POST /signup with {"username": "ada", "password": "correct horse"}. You get status 201 and {"username": "ada"}. Send the same body with "username": "bob". Now send GET /debug/users/ada, then GET /debug/users/bob. Each passwordHash is 32 characters, a colon, and 128 more characters. The two values are different, even though ada and bob chose the same password.
Try it yourself
import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);
// Turns a password into the text signup stores.
// For now it keeps the password as typed.
export async function hashPassword(password) {
return password;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Auth and Security
1Passwords
Bytes and Hex TextDeriving a Password KeyA Fresh SaltNever Store PasswordsComparing Key BytesChecking a PasswordSignup RulesPractice on your own: Express playground