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
passwordandpassword_confirmation. - Outbound HTTPS access from the server if you use
uncompromised().
Step 1: Add the rule to a form request
php artisan make:request RegisterUserRequest<?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
<?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().
<?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:
'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:
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.
$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
hashedcast. - 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?
- Submit the form with
password123. It should fail on mixed case and symbols. - Submit a very common password that still satisfies the composition rules. If it appears in known breaches,
uncompromised()should reject it with thepassword.uncompromisedmessage. - 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.



