Laravel · Password breach checks
Validate breached passwords with a Laravel rule
NotBreachedPassword implements Laravel's ValidationRule interface, so you use it like any other rule object in a FormRequest or Validator::make call. The validate method sends the value to /v1/breaches/password through the Http::finowl() macro with a five second timeout, and calls $fail when the response says breached is true. The message uses the :attribute placeholder, so it reads naturally whatever the field is called.
Failures that are not the user's fault go through a single unavailable method. With failOpen set to true, a timeout, rejected key or rate limit is logged and the password is accepted. Pass false to the constructor for stricter flows such as admin accounts. A 400 is deliberately silent, because it only happens for empty passwords or ones longer than 4096 characters, which min and max rules already explain.
Steps
- 1
Register the Http::finowl() macro and add the finowl entry to config/services.php.
- 2
Create the rule with php artisan make:rule NotBreachedPassword and paste in the example.
- 3
Add new NotBreachedPassword() after min and max in your registration and password reset rules.
- 4
Run php artisan config:cache in production so the key is read from cached configuration.
- 5
Submit a common password in a local form to confirm the validation message appears.
php artisan make:rule NotBreachedPassword
php artisan make:listener CheckEmailBreaches --event="Illuminate\Auth\Events\Registered"
php artisan make:job EnrichCustomerGender<?php
// app/Rules/NotBreachedPassword.php
namespace App\Rules;
use App\Exceptions\FinowlException;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class NotBreachedPassword implements ValidationRule
{
public function __construct(private bool $failOpen = true) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$response = Http::finowl()->timeout(5)->post('/v1/breaches/password', [
'password' => (string) $value,
]);
} catch (ConnectionException) {
Log::warning('Finowl password check timed out');
$this->unavailable($fail);
return;
}
if ($response->successful()) {
if ($response->json('breached')) {
$fail('This :attribute has appeared in a data breach. Please choose a different one.');
}
return;
}
// Log the status and code only, never the submitted value.
$error = FinowlException::fromResponse($response);
match ($error->status) {
400 => null, // empty or over 4096 characters: let max/min rules explain
401 => Log::error('Finowl rejected FINOWL_API_KEY'),
429 => Log::warning('Finowl limit hit', ['code' => $error->errorCode, 'retry_after' => $error->retryAfter]),
default => Log::warning('Finowl password check failed', ['status' => $error->status]),
};
if ($error->status !== 400) {
$this->unavailable($fail);
}
}
private function unavailable(Closure $fail): void
{
if (! $this->failOpen) {
$fail('We could not check this password right now. Please try again shortly.');
}
}
}
// In a FormRequest:
// 'password' => ['required', 'string', 'min:12', 'max:4096', 'confirmed', new NotBreachedPassword()],IN PRODUCTION
Production tips
Add bail to the field's rules if you want to skip the API call for short passwords. Laravel then stops at the first failing rule, so min:12 failures never reach Finowl or spend a credit.
Keep password in the list of inputs Laravel never flashes to the session, as it is by default, so a failed validation redirect never stores the value in session data.
Fake the endpoint in tests with Http::fake and a breached false response, so registration feature tests stay fast and never depend on the network.
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 Laravel with a queued listener
- Queue gender enrichment jobs in Laravel
- 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
Validate breached passwords with a Laravel rule
Grab an API key, paste the example and run it. 100 free credits every month.