Strengthen the password requirements of your Laravel application with minimal effort

An easy security boost within minutes, for free

During a conversation with a client on the eve of their application rollout to the rest of the organization, the topic of user account security came up. We reflected on the idea that accounts are only as secure as the measures taken by users (choosing strong passwords, enabling two-factor), and the decision on whether to enforce these measures. When I suggested boosting the password requirements to be more strict instead of relying on users to choose strong passwords, it was a zero-hesitation decision with a high perceived value by the business-minded client.

I was excited to finally make use of the more advanced password rules Laravel has to offer, which up to that point I had only read about here and there. But then I thought, "Shouldn't I be doing this in all projects, to begin with?"

The quick version

Before deploying your application, take a moment to update the default password rules to at least something stronger than the defaults. Do this by customizing the application-wide password rules inside App\Actions\Fortify\PasswordValidationRules, and leverage the convenient pre-built validation rules provided by Laravel's Password rule object.

The detailed version

If you're working in an application backed by Fortify, the password rules are in the App\Actions\Fortify\PasswordValidationRules trait, which you are free to customize. If you are not using Fortify, keep reading, you will still be able to implement the principles demonstrated here. At the time of this writing (Laravel 9.x), this is the default content of the trait:

<?php

namespace App\Actions\Fortify;

use Laravel\Fortify\Rules\Password;

trait PasswordValidationRules
{
    /**
     * Get the validation rules used to validate passwords.
     *
     * @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    protected function passwordRules(): array
    {
        return ['required', 'string', new Password, 'confirmed'];
    }
}

By default, the ruleset includes an instance of Fortify's Password rule object (Laravel\Fortify\Rules\Password) which provides a few customizations:

(new Password)
    ->length(8) // minimum length of the password
    ->requireUppercase() // at least one uppercase character required
    ->requireNumeric() // at least one numeric digit required
    ->requireSpecialCharacter(); // at least one special character required

You may also swap it out with Laravel's native Password rule object (Illuminate\Validation\Rules\Password) that offers more:

Password::min(8) // minimum size of the password
    ->mixedCase() // require at least one uppercase and one lowercase letter
    ->numbers() // require at least one number
    ->symbols() // require at least one symbol
    ->letters() // require at least one letter
    ->uncompromised(threshold: 0); // password has not been compromised in data leaks

The two rule objects offer mostly the same capabilities, except for uncompromised() only available in the latter.

Putting it all together, here's what your application's customized PasswordValidationRules trait might look like:

<?php

namespace App\Actions\Fortify;

use Illuminate\Validation\Rules\Password;

trait PasswordValidationRules
{
    /**
     * Get the validation rules used to validate passwords.
     *
     * @return array
     */
    protected function passwordRules()
    {
        return [
            'required',
            'string',
            Password::min(8)
                ->mixedCase()
                ->numbers()
                ->symbols()
                ->letters()
                ->uncompromised(),
            'confirmed',
        ];
    }
}

This is such an effortless update to make thanks to the framework, yet it's easy to overlook.

Not using Fortify

If authentication is not powered by Fortify (e.g., Laravel Breeze), then the core principles above are still applicable wherever password creation and updates are being validated. You may instead establish your application's custom password rules via Password::defaults():

// In the boot() method of AuthServiceProvider:

Password::defaults(
    fn () => Password::min(8)
        ->uncompromised()
);

Then wherever password rules are to be applied, simply use Password::defaults(). Laravel Breeze, for example, already does this.

Provide the end user with good messaging

After you customize the password rules, be sure to go the extra mile by also offering appropriate messaging on frontend forms wherever a password gets chosen or updated (e.g., profile update, user registration). In the following sample registration page, I've added secondary text underneath the Password field informing the user of the password expectations.

A sample user registration form containing name, email, password and confirm password fields. The password field includes secondary text informing the user of the expected password rules ahead of time.

Choose the wording as you wish, as long as you offer something for the user to know the password requirements ahead of time. This way, you avoid them having to find out through potentially repeated validation errors, which will be a frustrating user experience.

In conclusion

When building applications for clients, being able to offer this kind of boost in security will be of huge perceived value to them from a non-developer perspective (freelancers, take note). I've been doing this long enough to know how annoying it would be to implement the above manually during my days before Laravel (or frameworks in general). It's one of the reasons I've come to love working with Laravel.