Checking a Password
Part of the Auth and Security section of Coddy's Node.js journey. Lesson 6 of 24.
A login derives a key from the attempted password using the saved salt and original key length. A fresh salt would give a different key even for the correct password. The original password is never decoded from storage.
// An invite code was stored as salt.keyHex
const [salt, keyHex] = savedInvite.split('.');
const expected = Buffer.from(keyHex, 'hex');
const attempted = await scryptAsync(inviteCode, salt, 32);Compare the resulting byte sequences using timingSafeEqual, after confirming that their lengths match. It avoids a content-dependent early exit that could reveal information through timing. Buffer === checks identity, and a normal string comparison provides no constant-time guarantee. Your password function uses the password record's colon delimiter and 64-byte key. The POST /login route you add uses the result to return 200 or 401.
Challenge
EasyThis step opens the course's prepared private notes API project, rather than your project files from Never Store Passwords (including passwords.js). Your earlier work stays in its own lesson. Read the supplied files before editing passwords.js. This copy contains password hashing helpers and the supplied debugging routes.
Signup now saves salted hashes. Add a login that checks a password against what signup saved.
- In
passwords.js, add and export anasyncfunctionverifyPassword(password, stored).storedis a value signup saved, likef2bb...f065:c3f7...dbd2. Returntruewhenpasswordis the password that made it, andfalseotherwise:- Take the salt from before the colon.
- Hash
passwordwith that salt into a 64 byte key, the same wayhashPassworddoes. - Decode the saved key after the colon from hex. If the two keys have different byte lengths, return
falsewithout callingtimingSafeEqual, as in Comparing Key Bytes. Otherwise return its comparison result.
- In
server.js, importverifyPasswordnext tohashPassword. AddPOST /login. It receives the same body as signup, for example{"username": "ada", "password": "correct horse"}.- Find the user with that username.
verifyPasswordis slow and returns a promise, so wait for it. - When the user exists and the password is right, respond with status 200 and
{"username": "ada"}. - Otherwise, respond with status 401 and
{"error": "Wrong username or password"}. 401 Unauthorized means the request did not prove who is asking. - Send exactly the same 401 for a wrong password and for a username nobody signed up with. Then the response body never tells anyone which usernames exist.
- Find the user with that username.
Try it: Press Run. Send POST /signup with {"username": "ada", "password": "correct horse"}: status 201. Send POST /login with the same body: status 200 and {"username": "ada"}. Send POST /login with {"username": "ada", "password": "wrong horse"}: status 401 and {"error": "Wrong username or password"}. Send POST /login with {"username": "nobody", "password": "correct horse"}: the same 401.
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.
export async function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const key = await scryptAsync(password, salt, 64);
return `${salt}:${key.toString('hex')}`;
}
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