Block compromised logins and passwords in your Laravel application
The DarkStrata Credential Check SDK for PHP ships a Laravel integration that stops compromised credentials being used on your site. When someone logs in or sets a password, the email and password pair is hashed locally and checked against the DarkStrata breach corpus using k-anonymity: only the first five characters of a SHA-256 hash ever leave your server. No personal data is sent: the email, username and password never leave your server, and the fragment cannot be reversed to identify anyone.
It is a single Composer install. The service provider is auto-discovered, so there is nothing to register, and the package writes nothing to your database. Supports Laravel 11, 12 and 13 on PHP 8.2 or newer.
What it does:
| Hook | Behaviour |
|---|---|
Login (Auth::attempt()) | After the password is verified and before the session starts, a compromised pair throws CompromisedCredentialException (default) or is allowed with a warning. Wrong passwords never cost an API call. |
| Password set, change or reset | Add the NotCompromisedCredential rule to the form. A compromised pair fails validation with the configured message. |
| Any hit | Dispatches CompromisedCredentialDetected so you can run your own workflow. |
| Health check | php artisan darkstrata:check confirms the key is configured and accepted. |
composer require darkstrata/credential-checkCreate an API key in app.darkstrata.io under Integrations > API keys with the credential_check:read scope and add it to .env:
DARKSTRATA_API_KEY=ds_live_...Then confirm it works:
php artisan darkstrata:checkThe defaults are sensible. To change anything, publish the config:
php artisan vendor:publish --tag=darkstrata-config| Setting | Env | Default | Meaning |
|---|---|---|---|
api_key | DARKSTRATA_API_KEY | - | Required. With no key every check is skipped and a warning is logged at boot. |
validate_passwords | DARKSTRATA_VALIDATE_PASSWORDS | true | Let the NotCompromisedCredential rule reject compromised pairs. |
check_logins | DARKSTRATA_CHECK_LOGINS | true | Check every successful login. |
login_action | DARKSTRATA_LOGIN_ACTION | deny | deny fails the login. warn allows it, logs a warning and dispatches the event. |
fail_open | DARKSTRATA_FAIL_OPEN | true | If the API is unreachable, allow the operation. false rejects instead. |
email_field | email | The credentials key and request field holding the email. | |
messages.login, messages.password | Wording shown to the person. Translate by publishing the config and using __(). |
Add the rule wherever a password is set. On registration and reset forms the email is read from the same request; for a change-password form pass it explicitly:
use DarkStrata\CredentialCheck\Laravel\NotCompromisedCredential;
// Registration or reset: the email is read from the same request
$request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Password::defaults(), new NotCompromisedCredential()],
]);
// Change password for the logged-in user: pass the email explicitly
$request->validate([
'password' => ['required', 'confirmed', new NotCompromisedCredential($request->user()->email, $request->user()->id)],
]);This differs from Laravel's built-in Password::uncompromised(), which asks Have I Been Pwned whether the password alone has ever leaked. DarkStrata checks whether this email and this password have appeared together, which is what credential-stuffing attacks actually use.
Nothing to wire up. CompromisedCredentialException is a ValidationException on the email field, so Breeze, Fortify, Jetstream and hand-written controllers that call Auth::attempt() show the configured message on the login form, and the failed attempt still counts towards your rate limiter.
If you would rather handle it yourself:
use DarkStrata\CredentialCheck\Laravel\CompromisedCredentialException;
try {
Auth::attempt($credentials);
} catch (CompromisedCredentialException $e) {
return redirect()->route('password.request')->with('status', $e->errors()['email'][0]);
}Every compromised credential dispatches CompromisedCredentialDetected. Listen for it to notify your security team, force a reset or lock the account:
use DarkStrata\CredentialCheck\Laravel\CompromisedCredentialDetected;
Event::listen(CompromisedCredentialDetected::class, function (CompromisedCredentialDetected $event) {
// $event->source: 'login' or 'password'
// $event->email, $event->userId
});| State | Behaviour |
|---|---|
| No key configured | One warning is logged at boot. Every check is skipped; logins and password changes behave as if the package were not installed. |
| Key rejected (401) or API unreachable | With fail_open true (default) the operation is allowed and a warning is logged. With false the login or password change is refused. |
Pause without uninstalling. Remove the API key and every check is skipped. Set login_action to warn to keep checking without blocking anyone, which is useful for a trial period.
Uninstall.
composer remove darkstrata/credential-checkDelete any NotCompromisedCredential rules and listeners you added. The package writes nothing to your database, so there is nothing else to clean up.
The package is open source under the Apache-2.0 licence. The same Composer package is also the plain PHP SDK for use outside Laravel. If you need a hand, our team is happy to help.