Menu
CoddyTech

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 icon

Challenge

Easy

Right 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.

  1. In passwords.js, change hashPassword(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 scrypt makes from the password and that salt, also written as hex: 128 characters. The third argument of scryptAsync is the key length in bytes.
    • Join the two with a colon. A saved value looks like this: f2bba0669d9fa2d203afc7e61bf8f065:c3f7cae352e023a8d4766da9a316b5a2c844100efff22c4079f89a8ab8512ac91c33227481c3afa88c3ba0e88347f9e3e0c22dad9d55f19688a350f71be8dbd2
  2. You do not need to change server.js. Its POST /signup already calls hashPassword and saves what it returns. It also has GET /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;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Auth and Security

Practice on your own: Express playground