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

Password Exposed Laravel Package

jord-jd/password_exposed

Check if a password has been exposed in a data breach using the haveibeenpwned.com API. Provides a simple password_exposed helper and a PasswordExposedChecker class, returning EXPOSED, NOT_EXPOSED, or UNKNOWN, with support for SHA1 hashes and PHP 7.1+.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the helpers load.

composer require jord-jd/password_exposed
php artisan tinker
>>> password_exposed('hunter2');   // 'exposed' | 'not_exposed' | 'unknown'

There is no service provider, no config file, and no facade. The package registers two global helper functions via Composer's files autoload (password_exposed, password_exposed_by_hash, plus the boolean-returning password_is_exposed / password_is_exposed_by_hash), so they are available the moment composer dump-autoload runs. Nothing to publish, nothing to configure.

Where to look first.

  1. README.md — the PasswordStatus switch example is the whole API surface.
  2. src/PasswordExposedChecker.php — the constructor reveals your three extension points: a PSR-6 CacheItemPool, a PSR-18 HTTP client, and a Certainty bundle.
  3. src/PasswordStatus.php — the three constants you branch on.

First use case: block breached passwords at registration. That is the highest-value, lowest-disruption placement. Resets and password changes are equally safe. Login-time enforcement is a separate product decision (it forces existing users into a reset flow) — don't ship it in the same increment.

// app/Rules/NotBreached.php
namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class NotBreached implements ValidationRule
{
    public function __construct(private bool $failClosed = false) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $exposed = password_is_exposed($value); // true | false | null on API error

        if ($exposed === true) {
            $fail('This password has appeared in a known data breach. Please choose another.');
        } elseif ($exposed === null && $this->failClosed && app()->isProduction()) {
            $fail('We could not verify your password right now. Please try again.');
        }
    }
}
'password' => ['required', 'confirmed', Password::min(12), new NotBreached(failClosed: true)],

Wire it into the shared rule set (Password::defaults(), Fortify's PasswordValidationRules trait, or Breeze's RegisterController/NewPasswordController) so registration, reset, and change all inherit it in one place.

Implementation Patterns

Centralise the policy, not the call. The EXPOSED / NOT_EXPOSED / UNKNOWN triad is a product decision, so put it in one class rather than sprinkling switch statements through controllers:

// app/Support/PasswordBreachPolicy.php
public function shouldReject(?bool $exposed): bool
{
    return match (true) {
        $exposed === true  => true,                        // breached: always block
        $exposed === null  => (bool) config('auth.hibp.fail_closed', false), // outage
        default            => false,
    };
}

Set fail_closed=false in production by default: HIBP outages should not lock you out of onboarding new users. Make it a config flag so the decision is documented and reversible.

Wrap it behind your own contract. The package exposes a concrete class with no interface, so bind your own so tests and future swaps don't touch call sites:

// AppServiceProvider::register()
$this->app->singleton(PasswordBreachChecker::class, fn ($app) => new PasswordBreachChecker(
    new \PasswordExposed\PasswordExposedChecker(
        // PSR-6 pool sharing Laravel's cache store
        new \Symfony\Component\Cache\Adapter\Psr16Adapter($app['cache']->store('hibp'))
    )
));

Check the constructor's parameter order in vendor/ — it has changed across majors, and positional injection will bite you on upgrade.

Lean on prefix-keyed caching. The package caches the raw HIBP range response under the first five SHA-1 characters, not the verdict. That means one API call warms the cache for every password sharing that prefix — on a real user base the hit rate is very high. Give it a persistent store (Redis/database), not array, or every request pays a round trip.

Handle the latency budget deliberately. A cache miss is a synchronous outbound HTTPS call (~100–300 ms, up to Certainty's 5 s timeout on a bad day). That is acceptable on registration/reset. It is not acceptable inside a loop, a queued bulk import, or a login hot path.

Check on next login, not by sweeping the table. You cannot retro-scan existing users: passwordExposedByHash() needs the SHA-1 of the plaintext, and your users' passwords are bcrypt/argon2. The only moment you have plaintext is the request itself. So if you need retroactive enforcement, do it opportunistically at successful login and flag the account for a forced reset — and only if the UX team has signed off.

Pin and migrate cleanly. This package replaces divineomega/password_exposed; if you're migrating, remove the old requirement rather than requiring both. On Laravel 11+ you need v5.1.0 or later so the PSR-6 v4/PSR Cache 3 adapters resolve — the older constraints will collide with modern symfony/cache.

Gotchas and Tips

  • Laravel already ships this. Password::uncompromised() (Laravel 8+) performs the same k-anonymity range request internally via Http. Adopt this package only if you want the tri-state status, the nullable boolean, injectable caching/HTTP, or you're supporting a codebase pinned below that. Don't accidentally enforce both — you'll double the timeout exposure.
  • UNKNOWN is the whole story. Any network error, timeout, or malformed response yields UNKNOWN (or null from the boolean helpers), never an exception. That is convenient and dangerous: a silent API failure looks identical to "not checked." Log at warning when status is UNKNOWN so you notice a persistent outage rather than assuming every password was screened.
  • Don't expect a native enum. PasswordStatus is a constants class (PHP 7.1 support predates enums). === comparisons and match arms on the constants work fine; ->name, ::from(), and instanceof do not.
  • Global function collisions. The package drops its function_exists guard, so declaring your own password_exposed() helper is a fatal redeclare. Namespace your helpers.
  • Read-only container filesystems break Certainty. The TLS bundle is fetched and cached at runtime; v5.0.1 added a writable runtime cache directory plus a file lock to tame concurrent rename() warnings. On Docker/Kubernetes, warm the bundle during build or point it at a writable path — otherwise your first registration after a deploy pays the download and may emit warnings under concurrency.
  • You own TLS verification if you inject your own client. Passing a custom PSR-18/Guzzle client is the right move for timeouts, proxies, and retries, but it can bypass Paragonie Certainty's validated CA bundle. Keep the bundle wired.
  • Never log the input. Passwords flow through your validation rule; check App\Http\Kernel / ValidatesRequests::$dontFlash covers your field names, and keep password out of any request-dump middleware. The SHA-1 digest is also worth treating as sensitive — the five-character cache key is deliberately short for that reason.
  • Don't build a public "is this password pwned?" endpoint. Even with k-anonymity it becomes a free oracle and a traffic amplifier. If you must expose it, throttle with RateLimiter and keep it behind auth.
  • Test against known vectors, not the network. password has SHA-1 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8 and is famously exposed; a random 40-char hex string is reliably clean. Better still, bind a fake implementation of your own contract in tests/ so CI never depends on HIBP availability.
  • Version/architecture edge cases. On 32-bit systems without the sodium extension, certificate-bundle verification is slow; the package falls back but you'll feel it. PHP 8.1–8.5 is covered by CI as of v5.0.3.
  • Licensing. LGPL-3.0. Fine for hosted applications; if you distribute PHP binaries or a packaged appliance, get the dynamic-linking question reviewed before you rely on it.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin