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
Set FINOWL_API_KEY in the environment of your Node.js server.
- 2
Copy breachedPasswordCount into the module that handles registration and password changes.
- 3
Run your own length and format rules first, then await the breach check.
- 4
Return a 422 or form error to the client when the count is greater than zero.
// 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
Related guides
- Check email breaches in Node.js with fetch
- Predict gender from a name in Node.js
- Check breached passwords in Python at signup
- Check breached passwords in TypeScript
- Check breached passwords in Go before storing them
- Check breached passwords in Java at registration
- Breached password checks for fintech accounts
- Breached password screening for e-commerce accounts
- Breached password checks for SaaS signup and resets
Screen breached passwords in Node.js
Grab an API key, paste the example and run it. 100 free credits every month.