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+.
Architecture fit
This is a thin, framework-agnostic Composer library — one helper function, one PasswordExposedChecker class, and a PasswordStatus enum (EXPOSED / NOT_EXPOSED / UNKNOWN). It has no Laravel service provider, no facade, no config file, and no published migrations. Architecturally that is fine for a Laravel app because the integration surface is a single synchronous call you can wrap in your own service class or validation rule; it is not a package that will auto-wire itself into the framework. Treat it as a library you call, not a package you install and configure.
The real substance is the HaveIBeenPwned Pwned Passwords v2 range API: SHA-1 the password, send the first five hex characters, match the suffix locally. That is roughly 40 lines of code plus caching. The package adds PSR-6 caching (hash-prefix keyed), a PSR-18 HTTP client, and TLS certificate validation via paragonie/certainty. So the evaluation question is honestly "adopt a maintained wrapper vs. write the same 40 lines in-house," not "adopt a security product."
Integration feasibility
Feasible, with two non-obvious friction points that a spike will surface immediately:
CacheItemPoolInterface. Laravel's cache is not PSR-6 (its repository is PSR-16-shaped in recent versions). You either bridge via a Symfony PSR-6 adapter or let the package fall back to its own file cache — which writes into a runtime directory and has a documented history of rename() contention and read-only-filesystem issues. In a containerized/ephemeral deploy, that fallback is the likeliest thing to break.Illuminate\Http\Client is not PSR-18. You will end up pulling guzzlehttp/guzzle plus the package's adapter, and that adapter has version-sensitive Guzzle 6/7 behavior (v5.1.1 exists specifically to fix exception semantics there).Neither is a blocker, but "just composer require it" is not the accurate story. Budget a half-day spike.
Technical risk
UNKNOWN is returned on API error/timeout, so you cannot lose data — but you can lose onboarding, and the default Certainty HTTP timeout was historically 5 seconds. A 5s tail on a signup request is a product incident, not just an error log.psr/cache v1/v2/v3, psr/http-message v1/v2, Symfony cache adapter v3 vs v4 conditional on PHP version, Buzz 1.4 for Symfony 8). Each is a maintenance signal, but collectively they mean a composer update on the wider app can move this package under you. Pin it.Key questions
UNKNOWN? (And is that policy the same for registration, reset, and any admin-driven password set?)EXPOSED or warn-and-allow? This is a product decision the package forces, not one it makes.Stack fit
Good but not native. It sits cleanly alongside PHP 7.1–8.5 and Laravel 8+, and the SHA-1-hash entry point (passwordExposedByHash) lets you keep the plaintext confined to the validation layer. The lack of a Laravel service provider means the idiomatic integration is: a small application-owned service class wrapping PasswordExposedChecker, a PasswordNotBreached validation rule, and a config entry for the warn/block policy. Roughly 100 lines of glue. This is the right place to put the swap point so the vendor library is replaceable.
Migration path
Nothing to migrate. There is no schema, no persisted state, no data model — the only state is a cache keyed on hash prefixes, which is disposable. Rollout is additive: ship the validation rule behind a config flag, enable on registration first, observe the UNKNOWN rate, then enable on password reset.
Compatibility
Watch three specific things: (a) the PSR-6 cache adapter version, which the package resolves conditionally based on PHP version — verify it agrees with whatever symfony/cache your app already locks; (b) the PSR-18/Guzzle pair, which must be compatible with the Guzzle version already in the lockfile; (c) paragonie/certainty, which fetches and writes a certificate bundle and warns loudly on 32-bit systems without the sodium extension. Pin the package to a minor version and treat upgrades as a reviewed change, given the constraint churn in the changelog.
Sequencing
EXPOSED and UNKNOWN behavior for each surface. Fail-closed on UNKNOWN means an HIBP outage blocks new signups — make that a signed-off decision, not a default.UNKNOWN ratio.Maintenance
Low code footprint, non-trivial dependency footprint. The package itself is a few hundred lines; the operational surface is Composer constraints, a PSR-6 pool binding, a PSR-18/Guzzle client, and a certificate-bundle writer. Plan on Dependabot-driven PRs landing against it periodically and requiring review, because upstream actively relaxes constraints. Because the wrapper is application-owned, an abandonment scenario costs an afternoon to replace.
Support
Community-only. GitHub issues, no SLA, no named maintainer commitment, no vendor contract. That is acceptable for a non-differentiating utility and unacceptable if anyone tries to represent this as a compliance control. Document internally that the underlying dependency is the free HaveIBeenPwned API — no key, no commercial agreement, no uptime guarantee.
Scaling
Favorable. Cache keys are 5-character hash prefixes; the keyspace is ~1M, so cache hit rate improves as your user base grows, and steady-state outbound traffic to HIBP is small and well within the free API's tolerance. Payloads are a few KB. The scaling constraint is not throughput — it is latency on the request path and the blast radius of the external call during traffic spikes, when timeouts will cluster.
Failure modes
UNKNOWN. Handled gracefully by design, but surfaces your fail-open/fail-closed policy at the worst possible time.vendor/, or concurrent container startup. The 5.0.1 changelog explicitly patches rename() contention with a file lock — evidence the failure is real.psr/cache, psr/http-message, or symfony/cache version skew breaking install or runtime after an unrelated dependency bump.Ramp-up
Very low. Any Laravel developer can integrate this in under a day. The non-obvious work is the spike (proving cache and HTTP client bindings resolve in your actual lockfile and runtime) and the policy sign-off, not the code. The two artifacts worth producing are the wrapper service class — which is your fork/swap point — and a monitoring dashboard tracking call volume, cache hit rate, and the UNKNOWN ratio, with an alert on the last one so an external outage shows up as a known, explained condition rather than a mystery spike in failed signups.
How can I help you explore Laravel packages today?