Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pwned Validator Laravel Package

assisted-mindfulness/pwned-validator

A Laravel validation rule that checks passwords against the Have I Been Pwned Pwned Passwords database using k-anonymity, so users can be blocked from choosing known-breached passwords without ever sending full credentials over the wire.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require assisted-mindfulness/pwned-validator

The package targets modern Laravel (9+) on PHP 8.1+ — confirm the exact constraints in its composer.json before adopting on an older app. There is no API key and no external account: it talks to the HaveIBeenPwned Pwned Passwords range endpoint (api.pwnedpasswords.com/range/{first5OfSha1}), which is free and unauthenticated.

Where to look first

  1. The README usage block — the rule is a standard Laravel ValidationRule object, so it drops into any rules() array.
  2. src/Rules/Pwned.php (or equivalent) — read it before shipping. You want to confirm two things yourself: that only the first five characters of the SHA-1 leave your server, and that the suffix comparison happens locally.
  3. config/pwned.php if present in your installed version — publish with php artisan vendor:publish --tag=pwned-config and re-run php artisan config:cache afterwards.

First use case

Registration and password-reset, via a FormRequest — not login:

use AssistedMindfulness\Pwned\Rules\Pwned;

public function rules(): array
{
    return [
        'password' => ['required', 'string', 'min:12', 'confirmed', new Pwned()],
    ];
}

That single line is the whole day-to-day surface area for most teams. Wire it into RegisterController / NewPasswordController and you're done; there is no middleware, no migration, no scheduled job.

Implementation Patterns

Centralise the rule, don't scatter it. Two FormRequests and a Nova/Livewire form will drift. Put the rule set on App\Support\Rules\PasswordRules::defaults() (or extend Laravel's Password::defaults() callback) and return a new Pwned($threshold) from one place. When you tune the threshold later you tune it once.

Use the threshold argument rather than a boolean stance. A password seen once in a decade-old leak is not the same risk as 123456. In practice:

  • new Pwned() (threshold 0) — reject anything present in any breach. Strict, and the right default for consumer signup.
  • new Pwned(5) / new Pwned(10) — only reject genuinely popular leaked passwords. Useful for internal/staff tools where false positives generate support tickets.

Whatever you pick, write it down in the error message: "This password has appeared in a known data breach" tests far better with users than a generic "invalid input".

Pair with, don't replace, the built-in rule. Laravel ships Password::min(8)->uncompromised(), which hits the same API. Adopting this package is justified when you want a composable rule object with an explicit threshold, deterministic test fakes, or framework-version independence. Don't run both — you'd double the outbound calls for identical results.

Testing pattern (the one that matters). The rule performs HTTP, so unmocked tests are slow and flaky and will fail in offline CI. Fake the range endpoint:

Http::fake([
    'api.pwnedpasswords.com/*' => Http::response(
        "0018A45C4D1DEF81644B54AB7F969B88D65:12\r\n00D4F6E8FA6EECAD2A3AA415EEC418D38EC:3",
        200
    ),
]);

$this->post('/register', ['password' => 'password', ...])->assertSessionHasErrors('password');

Compute a known hash prefix/suffix pair once as a fixture so the assertion is real rather than incidental. Add Http::preventStrayRequests() to the suite-wide TestCase::setUp() so an accidental live call fails loudly. If the package does not use the Laravel HTTP client in your installed version, bind a fake implementation of its client class in the container instead — check src/ before writing the fake.

Decide fail-open vs fail-closed explicitly. Wrap production usage so an HIBP outage doesn't take down registration. Either configure short timeouts plus a retry, or catch the connection exception in a small decorator rule and log-and-pass. Failing closed on a third-party's availability is a business decision, not a default.

Where not to apply it. Never on the login form — existing users with leaked passwords would be locked out. Never on API token or webhook-secret generation (no human password, nothing to check). Do apply to password change and reset flows, and remember reset flows bypass RegisterRequest, which is the classic gap.

Livewire/Inertia. Validate on submit, not on wire:model keystrokes — you'd add 100–300 ms of latency per character and hammer a free public API.

Gotchas and Tips

Latency is user-visible. Each validation is a blocking outbound HTTPS round-trip. Set a connect timeout (~2s) and total timeout (~5s); leaving Guzzle defaults means a degraded HIBP shows up as a hung signup form.

Audit the k-anonymity yourself. The security story rests entirely on only the 5-char prefix being transmitted and the suffix being matched client-side. This is a 20-line read of the rule class; do it once, and re-do it on any major version bump.

Don't pre-hash or post-process the input. Pass the plaintext as the user typed it. SHA-1, uppercasing, and prefix/suffix splitting are the package's job. Passing an already-hashed value silently validates the wrong string and always "passes".

Rule ordering and empty values. Non-implicit rules are skipped when the value is empty, so new Pwned() won't fire on null — good. But if you use sometimes, exclude, or nullable on the field, be sure the rule still runs when the password is being set. And keep required|string|min: ahead of it so cheap checks fail first.

Never let the password reach your logs. Validation failures are exactly the moment error trackers capture request payloads. Configure your Sentry/Bugsnag/Telescope instance to scrub password, password_confirmation, and current_password.

Config caching is the usual "my threshold didn't change" bug. Publish → edit → php artisan config:cache. In deployments where config is cached at build time, env-var overrides at runtime do nothing.

Response parsing quirk to be aware of. HIBP pads every range response with hundreds of decoy hashes so an observer can't infer hit volume from payload size. Don't write assertions or monitoring around response length, and don't treat a large 200 response as suspicious.

Extension points. Subclass the rule to override message() (cheapest customisation, better UX copy), or wrap it in your own ValidationRule to add the try/catch fail-open behaviour and a withValidator-style bypass for seeded/admin accounts. Avoid forking the package for either.

Versioning. 1.0.0 is the API-stability marker: constrain to ^1.0 and treat any future major as a review trigger — specifically re-audit the hashing path, since that's where a semver-breaking change would most plausibly land. The release notes themselves are just a changelog link, so if you ran a 0.x build previously, diff the rule's namespace/constructor signature (new Pwned() vs. the older signature) before upgrading rather than assuming drop-in compatibility.

Have an offline story. Corporate networks and some CI runners block api.pwnedpasswords.com. If you use Docker in tests, either fake the HTTP layer for every test that touches registration or gate the rule behind an env flag — otherwise a firewall becomes a failing build.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style