Skip to content
Finowl

Node.js · Password breach checks

Screen breached passwords in Node.js

Credential stuffing works because people reuse passwords that already leaked somewhere else. Checking new passwords in your Node.js backend closes that gap at the moment it matters. The endpoint takes the password in the JSON body and answers with breached and count. Finowl hashes it with SHA-1, searches using only a 5-character prefix, and never logs, stores or returns the value.

The function below is written to be safe inside Express, Fastify or Next.js route handlers. Errors carry the status and code but never the password, and the request body is built inline so it is not kept in a variable that a logger might serialize. The demo reads input from stdin with node:readline/promises so you can try it locally before wiring it into a route.

Steps

  1. 1

    Set FINOWL_API_KEY in the environment of your Node.js server.

  2. 2

    Copy breachedPasswordCount into the module that handles registration and password changes.

  3. 3

    Run your own length and format rules first, then await the breach check.

  4. 4

    Return a 422 or form error to the client when the count is greater than zero.

Screen breached passwords in Node.js
// check-password.mjs (Node.js 18+)
import { createInterface } from "node:readline/promises";
import { setTimeout as sleep } from "node:timers/promises";

// Returns how many times the password appears in breach data.
// Never log the password or the request body.
export async function breachedPasswordCount(password, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch("https://api.finowl.co/v1/breaches/password", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + process.env.FINOWL_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ password }),
      signal: AbortSignal.timeout(5_000),
    });
    if (res.ok) {
      const { breached, count } = await res.json();
      return breached ? count : 0;
    }
    const payload = await res.json().catch(() => ({}));
    const code = payload?.error?.code ?? "unknown_error";
    if (res.status === 400) throw new Error("Password failed validation (" + code + ")");
    if (res.status === 401) throw new Error("Unauthorized: check FINOWL_API_KEY");
    if (res.status === 429 && code === "rate_limited") {
      await sleep((Number(res.headers.get("retry-after")) || 1) * 1000);
      continue;
    }
    throw new Error("Password check failed: " + res.status + " " + code);
  }
  throw new Error("Password check still rate limited");
}

const rl = createInterface({ input: process.stdin, output: process.stdout });
const candidate = await rl.question("Password to check: ");
rl.close();

const count = await breachedPasswordCount(candidate);
console.log(
  count > 0
    ? "Seen " + count.toLocaleString() + " times in breaches. Choose another password."
    : "Not found in breach data."
);

IN PRODUCTION

Production tips

  • Audit request logging middleware such as morgan or pino-http. Make sure it never serializes req.body on auth routes, or the password can end up in logs before Finowl is even called.

  • Use a short timeout, like the 5 seconds shown, so a stalled lookup does not hold a signup open. Decide whether to allow the password and recheck later or ask the user to retry.

  • Avoid caching results keyed by password. Each check costs one credit, but caching plaintext or fast hashes of passwords in Redis creates a far bigger risk than the cost saved.

Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Screen breached passwords in Node.js

Grab an API key, paste the example and run it. 100 free credits every month.