Menu
CoddyTech

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 icon

Challenge

Easy

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

  1. In passwords.js, add and export an async function verifyPassword(password, stored). stored is a value signup saved, like f2bb...f065:c3f7...dbd2. Return true when password is the password that made it, and false otherwise:
    • Take the salt from before the colon.
    • Hash password with that salt into a 64 byte key, the same way hashPassword does.
    • Decode the saved key after the colon from hex. If the two keys have different byte lengths, return false without calling timingSafeEqual, as in Comparing Key Bytes. Otherwise return its comparison result.
  2. In server.js, import verifyPassword next to hashPassword. Add POST /login. It receives the same body as signup, for example {"username": "ada", "password": "correct horse"}.
    • Find the user with that username. verifyPassword is 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.

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

Practice on your own: Express playground