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

nowo-tech/password-strength-bundle

Password-strength bundle for PHP/Symfony apps: estimate password quality, enforce configurable rules and policies, and return clear feedback for forms and APIs. Simple service and validator integration with sensible defaults.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first as a reference implementation, not a dependency. nowo-tech/password-strength-bundle is a Symfony bundle: it extends Symfony's PasswordType, ships a validator constraint with configurable strength policies, emits an HTML pattern attribute, and includes frontend assets for live feedback. There is no Laravel service provider, no config publish tag, no facade, and no illuminate/* dependency. composer require in a Laravel app will install code you cannot wire up without a Symfony runtime.

So the practical first step is: open the constraint/validator class, the form type extension, and the DI config section, in that order.

  1. Validator constraint — this is the policy model. Note how thresholds (minimum score, required character classes, length bounds, dictionary/common-password check) are expressed as options rather than hardcoded regex.
  2. Form type extension — note the options it adds to PasswordType and how the same policy drives both the server-side constraint and the rendered pattern attribute.
  3. DI config — note which keys are user-tunable. That key list is your config/password.php blueprint.
  4. Twig template + assets — skim only for the UX shape (meter, requirement checklist, live validation). Do not lift compiled build output.

First use case: replacing the usual scattered ['required', 'string', 'min:8', 'regex:/[A-Z]/', 'regex:/\d/'] soup on registration and password-reset with a single named policy that also feeds the client-side meter.

Your day-one equivalent in Laravel:

// app/Providers/AppServiceProvider.php  (the "DI config" analogue)
public function boot(): void
{
    Password::defaults(fn () => Password::min(12)
        ->mixedCase()
        ->numbers()
        ->symbols()
        ->uncompromised());
}

// app/Http/Requests/RegisterRequest.php
public function rules(): array
{
    return ['password' => ['required', 'confirmed', Password::defaults()]];
}

That gets you the server-side half of what the bundle does. The remaining value you'd port is the shared policy object and the client meter generated from it — covered below.

Implementation Patterns

1. One policy object, three consumers. The bundle's core idea is that the server rule, the HTML pattern, and the live meter all derive from the same configuration. Replicate that with a small value object:

final readonly class PasswordPolicy
{
    public function __construct(
        public int $minLength = 12,
        public int $minScore = 3,
        public array $requiredClasses = ['lower', 'upper', 'digit', 'symbol'],
        public bool $checkBreached = true,
    ) {}

    public function pattern(): string { /* build once, reuse in Blade */ }
    public function rules(): array { /* Password::min(...)->... */ }
}

Bind it as a singleton from config/password.php. Now Blade::component('password-field', [...]), your FormRequest, and your JS entrypoint all read the same numbers, and a policy change is a one-line config edit.

2. Wrap it in a custom rule for composite checks. Laravel's Password rule covers length/classes/breach but not "score above threshold" or "not similar to email/name". A ValidationRule around the policy keeps the FormRequest declarative:

final class StrongPassword implements ValidationRule
{
    public function __construct(private PasswordPolicy $policy) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // delegate to Password::defaults(), then add zxcvbn-style scoring
        // and reject values too similar to $this->request->input('email').
    }
}

Return a list of unmet requirements in the failure message only if you want the checklist UX; otherwise keep it generic.

3. Mirror the live feedback, don't gate on it. The bundle's JS is a UX hint. Keep the server authoritative: the meter colours the bar and toggles a submit-button disabled state at most, and you always re-validate server-side. Handle input, paste, and autofill (see Gotchas). A ~60-line module in your existing Vite entrypoint beats importing a foreign asset tree.

4. Generate the pattern attribute from the policy. Progressive enhancement only — it catches typos before a round trip. Set it via the Blade component so it never drifts from rules().

5. Apply on set, not on verify. Register, password reset, and change-password get the strong rule. The login endpoint gets required|string and nothing else. This is the single most important structural decision, and it's easy to get wrong when porting a validator that was designed to attach to a form type.

6. Roll out in warn mode. Add the rule, log the failures (Log::info('weak_password', ['policy' => $id, 'user_id' => $id]) — never the password), then enforce after a week. Prevents a support spike when the threshold moves.

7. Test with a dataset, not one case. Pest/PHPUnit datasets of weak/strong values asserting pass/fail and the exact error payload; a snapshot test on pattern(); a browser-level test that paste and autofill trigger the meter.

Gotchas and Tips

  • Don't validate strength on login. Users with pre-policy passwords can never sign in. Validate on create/change only; offer an optional "strengthen your password" nudge after successful auth.
  • uncompromised() makes outbound HTTP calls to the Have I Been Pwned range API (k-anonymity). That means latency on every registration, a hard dependency on network egress, and failures in offline CI. Cache positive results, decide explicitly between fail-open (skip the check on timeout) and fail-closed, and fake the HTTP client in tests.
  • Client/server drift is the classic failure. Every time someone tweaks min:12 in the FormRequest and forgets the JS, the meter lies. Derive the JS config from the same policy — emit it as a JSON blob in a data- attribute or a small inline config.
  • HTML pattern ≠ PHP regex. Anchor both ends explicitly, watch escaping inside the attribute, and remember PHP's Unicode handling differs from browser engines (notably the newer v flag vs u). Test in real browsers; treat the pattern as a hint, never as the rule.
  • Don't lift Symfony build artifacts. If the bundle ships compiled JS/CSS under a public/build path, take the concept and rewrite it against your existing Vite entrypoint. Vendoring foreign bundles into a Laravel asset pipeline is a maintenance trap.
  • Byte-vs-character length. Bcrypt truncates at 72 bytes; min/max in Laravel count characters; maxlength in HTML counts UTF-16 units. Cap length (e.g. max:72 or max:255 with a modern hasher) and don't silently truncate — a password that "fits" in the browser may not fit the hash.
  • Autofill and paste break naive meters. Password managers fill fields without firing keyup. Bind input and change, and never block paste — it's an accessibility and usability regression that also pushes users toward weaker, typeable passwords.
  • Leaking policy in error messages. Telling an attacker "needs 1 symbol and is not in the breach list" is fine; telling them the exact dictionary and score weights is not. Show the requirement checklist, keep the scoring internals server-side.
  • Adoption risk. Zero stars, zero dependents, and a release date that looks ahead of the current calendar — treat it as unmaintained for planning purposes: no CVE feed, no Laravel support, no upgrade guarantees. Don't pin it as a dependency; port the ideas (policy object, shared config, live meter) into app/Support and keep them under your own tests.
  • Bundled Symfony Twig templates are not Blade. Helpers like form_widget, trans filters, and asset() don't translate one-to-one. Rewrite the markup as a Blade component rather than translating line by line.
  • Debugging the rule. Assert it in isolation (php artisan tinker, or a unit test calling validate() directly) before wiring into the FormRequest — Laravel's ValidationException message array makes it hard to tell which sub-check rejected once several are composed.
  • Extension points worth mirroring: a configurable policy object, a translatable failure message per unmet requirement, a reusable Blade partial for the meter, and an injectable scoring strategy — so a future "strict mode for admins" is a binding change, not a rewrite.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect