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 Strength Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. Validator/Constraints/PasswordStrength.php — the constructor options (minLength, maxLength, requireLetters, requireNumbers, requireCaseDiff, requireSpecialCharacter, allowWhitespace). These options are your requirements spec.
  2. Validator/Constraints/PasswordStrengthValidator.php — the actual preg_match/strlen checks. This is the whole package, roughly 50–100 lines.
  3. 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.

Implementation Patterns

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.

Gotchas and Tips

  • Composer will block you loud, not silent. A real 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.
  • Pre-NIST rule model. Composition rules (upper + lower + digit + special) are explicitly discouraged by NIST SP 800-63B, which favors length floors plus a breached-password blocklist. Porting this algorithm verbatim can regress your posture while looking like a hardening measure. Prefer Password::min(12)->uncompromised() as the default and use the bundle's rule options only as a migration artifact.
  • Rejecting passphrases is the most likely user-facing failure. Test 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.
  • Multibyte handling is broken in PHP 5.3-era regexes. The original likely lacks the /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.
  • Do not log the plaintext. Laravel's 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.
  • Use 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.
  • Wire every credential endpoint, not just registration. Password reset and self-service password change are easy to miss, and they are the endpoints most likely to be used in support-ticket abuse. Centralizing via Password::defaults() prevents this.
  • Don't forget hashing assumptions. Policy min length must be compatible with your 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.
  • Extension point for a spike. If the requirement is genuinely "more than Laravel's 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 before vendoring. Confirm the repo's 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.
  • Signal quality. 28 stars and zero dependents is a strong abandonment tell — no issue triage, no security response path, no PHP 8 fixes coming. Any fork is permanently yours. That is fine for ~200 lines of deterministic validation logic extracted as a port; it is unacceptable as a runtime dependency in an authentication path.
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.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names