Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

How to Prevent Weak Passwords in Laravel 13 with the Password Rule

๐Ÿ› ๏ธ Laravel Tutorials5 min read

How to Prevent Weak Passwords in Laravel 13 with the Password Rule

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 7, 2026
How to Prevent Weak Passwords in Laravel 13 with the Password Rule

To stop users choosing weak passwords in Laravel 13, validate the field with the Password rule object: set a minimum length, require letters, mixed case, numbers and symbols, and add uncompromised() to reject passwords found in known data breaches. This guide targets Laravel 13 and PHP 8.3 or newer.

What is the Password rule in Laravel?

Illuminate\Validation\Rules\Password is a fluent validation rule. You build the policy by chaining methods, then pass it to your validator. It works the same in controllers, form requests and Livewire or Fortify actions.

Prerequisites

  • Laravel 13 on PHP 8.3 or newer.
  • A registration or password-change form that posts password and password_confirmation.
  • Outbound HTTPS access from the server if you use uncompromised().

Step 1: Add the rule to a form request

Code
php artisan make:request RegisterUserRequest
JavaScript
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;

class RegisterUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'max:255', 'unique:users,email'],
            'password' => [
                'required',
                'confirmed',
                Password::min(10)
                    ->letters()
                    ->mixedCase()
                    ->numbers()
                    ->symbols()
                    ->uncompromised(),
            ],
        ];
    }
}

Each method adds one requirement:

  • min(10) sets the minimum length. Longer is better than complex.
  • letters() needs at least one letter.
  • mixedCase() needs at least one uppercase and one lowercase letter.
  • numbers() and symbols() need at least one of each.
  • uncompromised() checks the password against the Have I Been Pwned range API using k-anonymity, so the full password never leaves your server.

Step 2: Use it in the controller

JavaScript
<?php

namespace App\Http\Controllers;

use App\Http\Requests\RegisterUserRequest;
use App\Models\User;
use Illuminate\Support\Facades\Auth;

class RegisterController extends Controller
{
    public function store(RegisterUserRequest $request)
    {
        $user = User::create([
            'name' => $request->validated('name'),
            'email' => $request->validated('email'),
            'password' => $request->validated('password'),
        ]);

        Auth::login($user);

        return redirect()->route('dashboard');
    }
}

The User model casts password to hashed in a default Laravel install, so the value is hashed automatically. Do not call bcrypt() again or the password is hashed twice.

How do you set one default policy for the whole app?

Repeating the chain in every form is error-prone. Define a default once in a service provider and reuse it with Password::defaults().

JavaScript
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Password::defaults(function () {
            $rule = Password::min(10)->letters()->mixedCase()->numbers();

            return $this->app->isProduction()
                ? $rule->symbols()->uncompromised()
                : $rule;
        });
    }
}

Now any form can use the default policy:

JavaScript
'password' => ['required', 'confirmed', Password::defaults()],

Making the policy lighter outside production keeps local seeding and tests fast, because uncompromised() makes an HTTP request.

How do you customise the error message?

The default messages come from the password keys in lang/en/validation.php. Publish the language files with php artisan lang:publish and edit them. You can also override a message in the form request:

JavaScript
public function messages(): array
{
    return [
        'password.min' => 'Use at least 10 characters.',
        'password.mixed' => 'Mix uppercase and lowercase letters.',
        'password.uncompromised' => 'This password appeared in a data breach. Choose another one.',
    ];
}

How do you validate a password change?

A profile page needs three fields: the current password, the new password and its confirmation. Combine current_password with the shared policy, and add different:current_password so users cannot reuse the same value.

JavaScript
$request->validate([
    'current_password' => ['required', 'current_password'],
    'password' => [
        'required',
        'confirmed',
        'different:current_password',
        Password::defaults(),
    ],
]);

$request->user()->update([
    'password' => $request->input('password'),
]);

Because the hashed cast is on the model, assigning the plain value is enough. After a password change, consider logging out other devices with Auth::logoutOtherDevices(), which needs the AuthenticateSession middleware to be enabled.

Why keep the policy in one place?

When the policy lives in Password::defaults(), registration, reset and change-password screens always agree. Changing the minimum length from 10 to 12 becomes a one-line edit, and your tests only need to cover one definition.

Common mistakes

  • Requiring complexity but allowing a six-character minimum. Length matters most.
  • Using uncompromised() in tests without faking HTTP. Tests then depend on a network call.
  • Forgetting confirmed, so a typo locks the user out of their new account.
  • Hashing the password manually when the model already uses the hashed cast.
  • Applying the strict rule on login. Validate strength only when a password is created or changed, never when it is checked.

How do you verify it works?

  1. Submit the form with password123. It should fail on mixed case and symbols.
  2. Submit a very common password that still satisfies the composition rules. If it appears in known breaches, uncompromised() should reject it with the password.uncompromised message.
  3. Submit a unique passphrase that meets the policy. The account should be created.

You can also assert this in a feature test with postJson('/register', [...])->assertJsonValidationErrors('password').

FAQ

Does uncompromised() send the password to a third party?

No. Laravel sends only the first five characters of the SHA-1 hash and compares the returned list locally, so the full password or full hash is not shared.

What happens if the breach API is down?

The check cannot confirm the password, so test how your app behaves when the request fails and decide whether you accept that risk in production.

Should I force symbols and numbers?

Current security guidance prefers length and breach checks over strict composition rules. Use the chain that fits your users, and keep the minimum length at least 10.

Can I use the same rule for password updates?

Yes. Use Password::defaults() on the new password field and the current_password rule for the old one.

References

Article Index & Topic Tags
LaravelSecurityAuthenticationValidation

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.