Laravel 12 brings a new, streamlined approach to enforcing strong validation rules—especially for sensitive fields like passwords—through the secureValidate()
method. In this article, we’ll cover:
- Why “enhanced validation” matters
- The difference between
validate()
andsecureValidate()
- How to configure your password policy with
Password::defaults()
- Examples using
secureValidate()
in controllers and Form Requests - Customizing validation rules beyond the defaults
1. Why “Enhanced Validation” Matters
In many applications, user-supplied data must meet strict security and formatting requirements. For instance, password fields often require:
- Minimum length (e.g. 8+ characters)
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one symbol
- Not appearing in known data breaches (uncompromise)
Prior to Laravel 12, developers had to manually stack multiple rules—for example, multiple regex
rules—to enforce complexity. This approach can be error-prone, difficult to maintain, and easy to forget in different parts of the codebase.
Laravel 12 introduces a concise way to declare “strong” validation via the new secureValidate()
method. When you use the strong
rule, Laravel applies a centralized password policy (configurable in one place). This leads to:
-
Better maintainability: Change your policy once and it applies everywhere the
strong
rule is used. - Cleaner controllers: No more copy-pasting complex regex chains.
- Built-in breach checks: Automatically ensures that passwords are not compromised in public data leaks.
2. validate()
vs. secureValidate()
In Laravel 11 and earlier, you would write something like:
// Before Laravel 12
public function register(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'password' => [
'required',
'string',
'min:8',
'regex:/[a-z]/', // at least one lowercase letter
'regex:/[A-Z]/', // at least one uppercase letter
'regex:/[0-9]/', // at least one digit
'regex:/[@$!%*#?&]/', // at least one symbol
'confirmed'
],
]);
User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
// ...
}
Notice how verbose the password
rules are: multiple regex
patterns and a minimum length. If you have multiple forms (e.g., registration, password reset, profile update), you’d duplicate those rules everywhere.
Enter secureValidate()
With Laravel 12, you can replace that entire block with:
// Laravel 12+
public function register(Request $request)
{
$data = $request->secureValidate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'strong', 'confirmed'],
]);
User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
// ...
}
Key points:
-
secureValidate()
is functionally identical tovalidate()
, except that it recognizes the specialstrong
rule. - When you declare
'strong'
, Laravel will automatically enforce a predefined set of password constraints (length, uppercase, lowercase, digit, symbol, uncompromised).
3. Configuring the Password Policy with Password::defaults()
By default, Laravel 12 comes with a sensible password policy, but you can customize it in your App\Providers\AppServiceProvider
(or any custom service provider):
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Password::defaults(function () {
return Password::min(8) // Minimum length = 8
->mixedCase() // At least one uppercase and one lowercase
->numbers() // At least one digit
->symbols() // At least one symbol (@$!%*#?&)
->uncompromised(); // Check against breach database
});
}
}
In this closure, you have several helper methods:
-
min(int $length)
: set minimum length. -
mixedCase()
: enforce at least one uppercase and one lowercase letter. -
numbers()
: require at least one numeric character. -
symbols(string $list = null)
: require at least one symbol (default set is @$!%*#?&). You can supply your own string of allowed symbols. -
uncompromised()
: check against Have I Been Pwned to ensure the password has not appeared in known data breaches.
You can chain or omit any of these methods to suit your project’s security requirements. For example, if you want a longer minimum length of 12 and allow only a custom set of symbols, you could write:
Password::defaults(function () {
return Password::min(12)
->mixedCase()
->numbers()
->symbols('!$%*#?')
->uncompromised();
});
Once this is set, all validations that include the strong
rule (whether via secureValidate()
or a Form Request) will inherit this policy.
4. Example: Using secureValidate()
in a Controller
Below is a full example of a registration controller using secureValidate()
:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class RegisterController extends Controller
{
public function showRegistrationForm()
{
return view('auth.register');
}
public function register(Request $request)
{
// 1. Run enhanced validation
$data = $request->secureValidate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'strong', 'confirmed'],
]);
// 2. Create and hash the password
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
// 3. Log the user in (optional) and redirect
auth()->login($user);
return redirect()->route('dashboard');
}
}
Then in your Blade template (resources/views/auth/register.blade.php
), you have:
<form method="POST" action="{{ route('register') }}">
@csrf
<div>
<label for="name">Name</label>
<input id="name" name="name" type="text" value="{{ old('name') }}" required autofocus>
@error('name')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" value="{{ old('email') }}" required>
@error('email')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label for="password">Password</label>
<input id="password" name="password" type="password" required>
@error('password')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label for="password_confirmation">Confirm Password</label>
<input id="password_confirmation" name="password_confirmation" type="password" required>
</div>
<button type="submit">Register</button>
</form>
Laravel will automatically display validation errors for all fields. If the password fails the “strong” policy, you’ll see a default error message like:
The password must be at least 8 characters and contain at least one uppercase letter, one lowercase letter, one number, one symbol, and must not have appeared in a data breach.
You can customize that message in resources/lang/en/validation.php
by overriding the password.stron
g key:
'password' => [
'strong' => 'Your password must be at least :min characters, contain upper- and lower-case letters, a number, a symbol, and must not appear in data leaks.',
],
5. Using secureValidate()
in a Form Request
If you prefer Form Requests for cleaner controllers, the process is almost identical:
1- Generate a Form Request:
php artisan make:request RegisterRequest
2- In app/Http/Requests/RegisterRequest.php
, define:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true; // adjust as needed
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'strong', 'confirmed'],
];
}
public function messages(): array
{
return [
'password.strong' => 'Password must be at least :min chars, include uppercase, lowercase, number, symbol, and not be compromised.',
];
}
}
3- Inject this Form Request into your controller action:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\RegisterRequest;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class RegisterController extends Controller
{
public function register(RegisterRequest $request)
{
// Rules from RegisterRequest include 'strong'
$data = $request->secureValidate();
User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
auth()->loginUsingId($user->id);
return redirect()->route('dashboard');
}
}
Because the rules are defined in RegisterRequest::rules()
, calling $request->secureValidate()
without arguments pulls those rules automatically.
6. Customizing Beyond the Defaults
6.1. Changing Minimum Length or Symbol Set
If you need a longer minimum length (e.g., 12) or a custom allowed-symbol set, adjust your provider:
// Inside AppServiceProvider@boot()
Password::defaults(function () {
return Password::min(12)
->mixedCase()
->numbers()
->symbols('!$@*#?')
->uncompromised();
});
Now, any validation with strong enforces at least 12 characters and requires one of !$@*#?
.
6.2. Disabling the Breach Check
If you don’t want to check against compromised-password lists (e.g., for performance or privacy reasons), simply omit ->uncompromised()
:
Password::defaults(function () {
return Password::min(8)
->mixedCase()
->numbers()
->symbols();
// No uncompromised() here
});
6.3. Adding Custom Regex or Additional Rules
If you require a rule that isn’t covered by strong
(e.g., no whitespace), you can combine strong
with any additional rule:
$data = $request->secureValidate([
'password' => ['required', 'string', 'strong', 'regex:/^\S+$/', 'confirmed'],
]);
This example ensures no whitespace (regex:/^\S+$/
) in addition to the default “strong” policy.
7. Summary
- Laravel 12’s
secureValidate()
replaces manual, error-prone password rules with a single strong rule. - Centralizing password constraints in
Password::defaults()
makes it easy to update your policy in one place. - By default, the
strong
rule enforces length, mixed-case, numbers, symbols, and breach checks—out of the box. - You can override or extend the policy using the helper methods on the
Password
class or by appending standard validation rules (e.g., customregex
).
With secureValidate()
, your controllers and Form Requests become concise, maintainable, and secure by default. Give it a try in your next Laravel 12 project!
Top comments (3)
Thank you @blamsa0mine for your wonderful and well-thought through and well-written article. May I ask if manual Controller validation is necessary when you're using a FormRequest?
// Rules from RegisterRequest include 'strong'
$data = $request->secureValidate();
The FormRequest's
rules()
method is automatically invoked to apply the defined rules on the submitted form fields. Kindly correct me if my line of thinking is wrong.You are right.
To be clear there is no secureValidate method.
The only thing real thing in the post is Password::defaults.
I didn't want to comment on this post, because I already commented on another post of the same author where another non existing method was shown. But I didn't want you to be confused.
Thank you @xwero for your response to my comment. I appreciate you setting the record straight on this issue as it creates confusion if left unresolved.