Next.js · Password breach checks
Check breached passwords in a Next.js server action
Server actions suit password checks because form data goes straight from the browser to your server code, with no API route of your own to secure. The signup action reads the password from FormData, enforces a minimum length, and asks Finowl's /v1/breaches/password endpoint whether it has been seen in breach data. When breached is true, the action returns an error object that useActionState hands back to the form.
The catch block logs only the status and error code, never the password or the FormData. A 400 is shown to the user because it means the password is longer than 4096 characters, while 401 and 429 are logged and the signup continues, failing open. redirect is called outside the try block on purpose, since Next.js implements it by throwing an error that the catch would otherwise swallow.
Steps
- 1
Create lib/finowl.ts and set FINOWL_API_KEY in .env.local without a NEXT_PUBLIC_ prefix.
- 2
Add app/signup/actions.ts and replace createUser with your own user creation and password hashing.
- 3
Build the form as a Client Component that calls useActionState with the signup action.
- 4
Render state.error in an element with role alert so screen readers announce it.
- 5
Try a common password locally and confirm the breach message appears.
npm install server-only// app/signup/actions.ts
"use server";
import { redirect } from "next/navigation";
import { finowl, FinowlError } from "@/lib/finowl";
import { createUser } from "@/lib/users"; // your own persistence
export type SignupState = { error?: string };
export async function signup(_prev: SignupState, formData: FormData): Promise<SignupState> {
const email = String(formData.get("email") ?? "");
const password = String(formData.get("password") ?? "");
if (password.length < 12) return { error: "Use at least 12 characters." };
try {
const { breached } = await finowl<{ breached: boolean; count: number }>(
"/v1/breaches/password",
{ password },
5_000,
);
if (breached) return { error: "This password has appeared in a data breach. Choose another." };
} catch (err) {
// Log status and code only; never the password or the form data.
if (err instanceof FinowlError) {
if (err.status === 400) return { error: "Passwords can be at most 4096 characters." };
if (err.status === 401) console.error("Finowl rejected FINOWL_API_KEY");
if (err.status === 429) console.warn(`Finowl ${err.code}, retry after ${err.retryAfter ?? "?"}s`);
}
// Fail open: an unavailable check should not block signups.
}
await createUser(email, password);
redirect("/welcome");
}
// app/signup/page.tsx
// "use client";
// const [state, action, pending] = useActionState(signup, {});
// <form action={action}> ... {state.error && <p role="alert">{state.error}</p>} </form>IN PRODUCTION
Production tips
Server actions are reachable by direct POST, so repeat every check inside the action. Client-side length hints help the experience but never replace the validation done on the server.
Hash the password with argon2 or bcrypt only after the breach check passes, and make sure your error tracker does not capture FormData if createUser throws.
Check on submit rather than on every keystroke. Each lookup spends a credit, and a check-as-you-type field multiplies requests without improving the outcome for most users.
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 a Next.js route handler
- Suggest gender from names in Next.js server actions
- Check breached passwords in Python at signup
- Screen breached passwords in Node.js
- Check breached passwords in TypeScript
- Check breached passwords in Go before storing them
- Breached password checks for fintech accounts
- Breached password screening for e-commerce accounts
- Breached password checks for SaaS signup and resets
Check breached passwords in a Next.js server action
Grab an API key, paste the example and run it. 100 free credits every month.