andrey-helldar/strong-password
A Laravel/PHP package for generating and validating strong passwords. It provides customizable strength checks and a Laravel validation rule to help developers enforce safer credentials, reject weak passwords, and improve account security with minimal setup. Designed for easy integration into forms...
1. Install and check compatibility before anything else. The package is archived (last release 2020-12-30), so composer require andrey-helldar/strong-password may fail on modern stacks. Run it with a constraint and read the error rather than forcing it:
composer require andrey-helldar/strong-password --dry-run # see if constraints resolve first
composer require andrey-helldar/strong-password
If it refuses to install on PHP 8.x / Laravel 9+, stop there and jump to the workaround in Gotchas — do not --ignore-platform-reqs a validation rule into production.
2. Find out how the rule is exposed. Open vendor/andrey-helldar/strong-password/src/. You are looking for one of two things:
Illuminate\Contracts\Validation\Rule (used as new StrongPassword()), orValidator::extend(...) registration in the service provider (used as the string 'strong_password').Also check whether the provider is auto-discovered (Laravel 5.5+ extra.laravel.providers in the package composer.json) and whether a config/lang file needs php artisan vendor:publish. This five-minute read tells you everything the README won't.
3. First use case: a registration FormRequest. Don't start in a controller — start where passwords actually live:
// app/Http/Requests/Auth/RegisterRequest.php
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'confirmed', new StrongPassword()],
];
}
// app/Http/Controllers/Auth/RegisterController.php
public function store(RegisterRequest $request)
{
$user = User::create([
'name' => $request->validated('name'),
'email' => $request->validated('email'),
'password' => Hash::make($request->validated('password')),
]);
return redirect()->route('dashboard');
}
Post a weak password at your registration endpoint and confirm you get a 422 with a password key in errors. That is the whole smoke test.
Centralise the policy, don't repeat the rule. The day-to-day win of a dedicated rule is that registration, password reset, and "change password" all agree. Put the rule in one place:
// app/Support/Validation/PasswordRules.php
final class PasswordRules
{
public static function make(): array
{
return ['required', 'string', 'max:255', new StrongPassword()];
}
}
Then 'password' => [...PasswordRules::make(), 'confirmed'] on register and 'password' => PasswordRules::make() on reset. When the policy changes, you edit one file.
Wrap the package in your own rule. This is the pattern I'd insist on for an archived dependency — isolate it behind a first-party class so replacing it is a one-file change:
// app/Rules/StrongPassword.php
namespace App\Rules;
use AndreyHelldar\StrongPassword\...\StrongPassword as BaseRule; // verify real FQCN from src/
final class StrongPassword implements \Illuminate\Contracts\Validation\Rule
{
private BaseRule $rule;
public function __construct() { $this->rule = new BaseRule(); }
public function passes($attribute, $value): bool
{
return $this->rule->passes($attribute, $value);
}
public function message(): string
{
return $this->rule->message();
}
}
Now every FormRequest references App\Rules\StrongPassword, and switching to Laravel's built-in Password rule later is a trivial diff.
Inline validators and API controllers. Same rule, different entry point:
Validator::make($request->all(), [
'password' => ['required', 'string', 'confirmed', new StrongPassword()],
])->validate();
For APIs, return the standard validation response and let the client read errors.password[0] — that message is your whole UX for the failure case, so make sure it is actionable (see Gotchas).
Mirror the policy on the frontend. The server rule is the authority, but users hate submitting to discover the rules. Expose the same requirements once:
// config/password.php — single source of truth for both sides
return [
'min' => 8,
'require' => ['mixed_case', 'numbers', 'symbols'],
];
Render hints from config in Blade, and optionally ship the same config to JS via a small endpoint or @json. Never reimplement the regex in JS — hints only, validation stays server-side.
Test the behaviour, not the package. Feature tests against the endpoint give you real coverage and survive a rule swap:
it('rejects a weak password', function () {
$this->post('/register', [
'name' => 'A', 'email' => '[email protected]',
'password' => 'password', 'password_confirmation' => 'password',
])->assertSessionHasErrors('password');
});
it('accepts a strong password', function () {
$this->post('/register', [
'name' => 'A', 'email' => '[email protected]',
'password' => 'cOrrect-Horse-9!', 'password_confirmation' => 'cOrrect-Horse-9!',
])->assertRedirect('/dashboard');
});
It is archived — treat it as a stopgap, not a foundation. No PHP 8 / Laravel 9+ fixes are coming. Before adopting, check whether your Laravel already covers this natively: since Laravel 8, Password::min(8)->mixedCase()->numbers()->symbols()->uncompromised() does the same job with zero dependencies and active maintenance. If you adopt the package anyway, pin the exact version in composer.json ("andrey-helldar/strong-password": "1.x", not ^1.0) and put a ticket in the backlog to migrate.
The package is a few dozen lines of logic — vendoring is legitimate. If install fails on your stack, copy the rule class into app/Rules/ (respecting the MIT licence and attribution), or replace it with Password::min(...). Do not let a 17-star archived dependency block a PHP upgrade.
Rule ordering matters. required must come before the strong-password rule, otherwise an empty field is judged on complexity and you get a confusing message instead of "The password field is required." Likewise confirmed requires a password_confirmation key — forgetting it produces a confirmed error, not a complexity error, and it's easy to misdiagnose.
Watch multibyte length counting. If the rule uses strlen rather than mb_strlen, a password of emoji or non-Latin characters may pass or fail unexpectedly. Write one test with a Unicode password to learn the actual behaviour rather than assuming.
TrimStrings middleware skips password and password_confirmation by default — good, because leading/trailing spaces are legitimate password characters. Don't "helpfully" add trim to the rule, and don't add string|max:255 without thinking: a max cap is fine for DB column safety, but a tight max (e.g. max:20) silently punishes passphrase users and password managers.
Make the failure message actionable. A generic "The password is not strong enough" produces support tickets. If the rule's message is vague, override it in your FormRequest (messages()), or fork the rule and return something specific ("Password must include at least one number and one symbol"). If the package ships translations, publish them (php artisan vendor:publish) and edit the lang/ file rather than hardcoding English in messages(). After editing a published config, run php artisan config:clear — cached config is the usual reason a change "did nothing."
Complexity rules are not the same as security. Current NIST guidance favours length and screening against breached-password lists over mandatory symbol classes. Use this rule as a floor, add ->uncompromised() (or the package's equivalent, if any) to catch known-breached passwords, and consider a 12-character minimum. Symbol requirements mainly increase reset rates and sticky notes.
Keep passwords out of your logs. Validation failures are fine, but confirm your Telescope / Laravel Debugbar / exception tracker isn't capturing the request payload on the registration route. Never dd($request->all()) on a password endpoint and leave it in a commit.
Hash once, in one place. The rule validates; it does not hash. Use Hash::make() in the controller/service, or a 'password' => 'hashed' cast on the model (Laravel 10+). A double-hashed password looks like a failed login with no validation error — a genuinely nasty hour of debugging.
Extension point if you need more policy. If the package's rule is a class, subclass it and override passes()/message() for extra checks (e.g. reject passwords containing the user's email or name). That's cleaner than stacking three more rules onto the field, which makes error messages ambiguous about which check failed.
How can I help you explore Laravel packages today?