jbafford/password-strength-bundle
A Symfony bundle for adding password-strength validation to forms and services. It helps enforce configurable strength requirements and gives developers a simple way to encourage safer user passwords without building custom validation logic.
This is a Symfony2 bundle, so there is nothing to composer require into a Laravel app. Attempting it will fail dependency resolution: the package pins Symfony 2.x components (~2.1–~2.8) which conflict with the Symfony 6.x/7.x components shipped by Laravel 10/11. The package also has zero Packagist dependents — no downstream project has ever validated it against a modern stack. Treat it as reference source, not a dependency.
"Getting started" means reading two files and porting their behavior into a Laravel rule:
Validator/Constraints/PasswordStrength.php — the constructor options (minLength, maxLength, requireLetters, requireNumbers, requireCaseDiff, requireSpecialCharacter, allowWhitespace). These options are your requirements spec.Validator/Constraints/PasswordStrengthValidator.php — the actual preg_match/strlen checks. This is the whole package, roughly 50–100 lines.Resources/translations/validators.en.yml — the rejection messages, which you'll copy into lang/en/validation.php.Port it into a modern rule in one sitting:
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class PasswordStrength implements ValidationRule
{
public function __construct(
private int $minLength = 8,
private bool $requireCaseDiff = true,
private bool $requireNumbers = true,
private bool $requireSpecial = false,
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || mb_strlen($value, 'UTF-8') < $this->minLength) {
$fail('validation.password.min')->translate(['min' => $this->minLength]);
return;
}
if ($this->requireCaseDiff && ! (preg_match('/\p{Ll}/u', $value) && preg_match('/\p{Lu}/u', $value))) {
$fail('validation.password.case_diff')->translate();
return;
}
if ($this->requireNumbers && ! preg_match('/\p{N}/u', $value)) {
$fail('validation.password.numbers')->translate();
return;
}
if ($this->requireSpecial && ! preg_match('/[^\p{L}\p{N}\s]/u', $value)) {
$fail('validation.password.special')->translate();
return;
}
}
}
First use case: registration and password-reset form requests. This is where you actually exercise the port day to day. Before going further, however, verify that Laravel's native Password rule does not already cover the requirement — since Laravel 8 it does:
'password' => ['required', 'confirmed', Password::min(12)->mixedCase()->numbers()->uncompromised()],
The bundle is useful only for confirming which specific rules were enforced in a legacy Symfony system you're migrating off.
Pattern 1 — Port as a standalone rule, then supersede it. Use App\Rules\PasswordStrength to reproduce the Symfony bundle's exact pass/fail behavior for parity during migration. Once fixture tests prove equivalent outcomes, switch the call sites to Laravel's native Password rule and delete the port. This keeps the migration reversible and lets you drop the port the moment native rules are confirmed.
Pattern 2 — Combine native + ported rule during migration. During the cutover you can enforce the strictest union of both rule sets, then relax to the Laravel-native set after fixtures are green:
'password' => [
'required', 'confirmed',
new PasswordStrength(minLength: 12),
Password::min(12)->letters()->numbers()->uncompromised(),
],
Pattern 3 — Centralize policy using Password::defaults(). Never duplicate strength config across registration, reset, and password-change endpoints. In AppServiceProvider::boot():
public function boot(): void
{
Password::defaults(function () {
$rule = Password::min(12)->uncompromised();
return app()->isProduction() ? $rule->mixedCase()->numbers() : $rule;
});
}
Then every credential submission uses ['required', 'confirmed', Password::default()] (or Password::defaults() if you want an instance).
Pattern 4 — Pin behavior with fixture tests before deleting the legacy path. Extract a table of password => expected result from the Symfony service, and lock it in as Pest/PHPUnit tests:
it('enforces the legacy minimum length', function () {
$v = Validator::make(
['password' => 'abc'],
['password' => ['required', new PasswordStrength(minLength: 8)]],
);
expect($v->fails())->toBeTrue()
->and($v->errors()->first('password'))->toContain('at least 8');
});
Include Unicode, whitespace-containing passphrases, and boundary-length cases in these fixtures.
Pattern 5 — Add richer scoring behind the same contract. If you need entropy scoring rather than binary pass/fail (the bundle cannot do this), wrap bjeavons/zxcvbn-php inside the same ValidationRule and use zxcvbn's score to call $fail. The rule interface stays the same, so nothing at the call sites changes:
if ($this->scorer->rate($value) < 3) {
$fail('validation.password.weak')->translate();
}
Pattern 6 — Real-time UX feedback is a separate surface. Server-side rejection alone is a poor signup experience — the bundle offers nothing here. Add a front-end meter (zxcvbn-js, or a lightweight entropy bar) and let it call the same backend policy via an AJAX endpoint if needed. Keep the final authority server-side.
composer require jbafford/password-strength-bundle will fail resolution against Laravel 10/11 and CI will stop — which is better than the alternative of a partially-working dependency. Do not attempt to override the constraint.Password::min(12)->uncompromised() as the default and use the bundle's rule options only as a migration artifact.correct horse battery staple and other space-containing inputs. Never reject whitespace unless there is a specific policy reason; allowWhitespace defaults from the Symfony era often do, and that default should be overridden./u flag and uses strlen or ctype_*. When porting, use mb_strlen($value, 'UTF-8') and \p{L}/\p{N} classes with /u. Add explicit test cases with non-Latin scripts and emoji.ValidationException messages do not include the raw value, but debug tooling (dd(), laravel/telescope payloads, \Log::info($request->all())) will. Confirm dontFlash covers password and password_confirmation, and audit exception handlers before launch.$fail(...)->translate() requires the lang key to exist. Copy the bundle's validators.en.yml strings into lang/en/validation.php under a password namespace; otherwise the key renders literally in the response.ValidationRule, not the legacy Rule contract. Laravel 10+ accepts closure-based ValidationRule (shown above). The older Rule interface still works but is on the slow path; don't cargo-cult it from old blog posts.Password::defaults() prevents this.Hash::make driver (bcrypt 72-byte limit is the usual trap). Validate before hashing, and if you raise min to a passphrase-friendly value, ensure the hasher config still matches.Password rule offers," the right extension is not resurrecting this bundle — it's wrapping zxcvbn-php in a custom rule. That gets you entropy scoring and dictionary checks behind the same interface.LICENSE (likely MIT) before copying source into app/Rules/. If you copy, cite the file in the class docblock as prior art; do not leave the port anonymous.How can I help you explore Laravel packages today?