What’s the number one cause of data breaches? Weak or stolen passwords.
In 2025, a huge majority of all successful cyberattacks on US businesses still begin with a single compromised user account.
That’s why Laravel’s authentication system has evolved. The old make:auth command is a thing of the past. The modern approach is a powerful, flexible, and secure system built to handle today’s threats, from API tokens to multi-factor authentication.
This guide is a deep dive into building production-ready authentication with modern Laravel. We’ll explore the key tools like Fortify, Breeze, and Jetstream, and show you the best practices for keeping your application and your users safe.
Table of Contents
Tip 1: Strategic Scaffolding Selection: A Comparative Analysis of Breeze and Jetstream
Choosing between Laravel Breeze and Laravel Jetstream is a key first step in any new project. The right choice depends on your project’s needs and how your team prefers to work. The main tip is to choose based on how you want to customize the code, not just the features you get.
Tip: Use Breeze for Simplicity and Direct Customization
Breeze is a simple starter kit that gives you the basic authentication features most applications need, such as login, registration, and password reset.
Its main feature is that it’s direct and transparent.
How to Customize It: When you install Breeze, it puts the authentication controllers and views directly into your project’s app and resources folders. To customize the registration process, you just edit the RegisteredUserController.php file. This is the standard Laravel MVC pattern that most developers are already familiar with.
When to Use It: Choose Breeze for small to medium-sized projects, for apps that need a lot of custom authentication logic, or when your team prefers the standard, direct way of working with Laravel controllers.
Tip: Use Jetstream for Advanced Features, But Know the Architecture
Jetstream is an advanced starter kit that comes with more features ready to use immediately, including:
- Two-Factor Authentication (2FA)
- Session Management
- Team Support
However, Jetstream’s architecture is different. It uses a “headless” backend called Laravel Fortify, so there are no authentication controllers to edit.
How to Customize It: To customize Jetstream, you modify special “Action” classes that are located in your app/Actions folder. For example, you would edit the CreateNewUser.php action to change the user creation logic. This is a clean approach, but it is an extra layer of abstraction that can be confusing if you are not used to it.
When to Use It: Choose Jetstream for large applications or SaaS products that need its advanced features out-of-the-box, and when your team is comfortable with its Action-based design.
Tip: Avoid the Legacy laravel/ui Package
The laravel/ui package was the old way of handling authentication in Laravel. While it still works, it is now considered a legacy tool.
For any new project in 2025, you should always choose Breeze as the modern, simple alternative.
Quick Decision Guide
| Feature | Laravel Breeze | Laravel Jetstream |
| Purpose | A simple starter kit for authentication. | A full application starter kit. |
| Advanced Features | None | 2FA, Session Management, API Support, Teams |
| How to Customize | Edit controllers and views directly. | Edit published “Action” classes. |
| Best For | Projects needing full control and customization. | Projects needing advanced features out-of-the-box. |
Tip 2: Choose Your Authentication Kit Wisely
When starting a new Laravel project, one of your first big decisions is choosing an authentication starter kit. The tip is to think about how you want to work with the code long-term, not just the features you get on day one. Your choice between Laravel Breeze and Laravel Jetstream will define how you customize your app.
Choose Breeze for Simplicity and Direct Control
Laravel Breeze is a simple, minimal starter kit. It gives you the basic features most applications need:
- User Login & Registration
- Password Reset
- Email Verification
The most important thing to know about Breeze is that it’s direct and transparent. When you install it, it places the authentication controllers and views right into your project’s folders.
The Tip: You customize Breeze by editing these controller and view files directly, just like any other part of a standard Laravel MVC application. If you want to change the registration logic, you open RegisteredUserController.php and edit it. This is the simple, familiar pattern that most Laravel developers already know.
Use Breeze when: You need full control, your project requires a lot of customization, or your team prefers the standard, direct way of working with Laravel.
Choose Jetstream for Advanced Features (But Understand the Architecture)
Laravel Jetstream is a more advanced starter kit with more features ready to use immediately. On top of all the basics, it includes:
- Two-Factor Authentication (2FA)
- Session Management
- API Support via Sanctum
- Optional Team Management
However, Jetstream’s architecture is different. It uses a “headless” backend called Laravel Fortify. This means it does not add authentication controllers to your app folder.
The Tip: You customize Jetstream by editing special “Action” classes that are published to your app/Actions folder. For example, to change how a new user is created, you edit the CreateNewUser.php file in that folder. This is a clean approach, but it’s an extra layer of abstraction. If you’re not familiar with it, it can be more complex to change or debug.
Use Jetstream when: You’re building a large app or SaaS product that needs its advanced features out of the box, and your team is comfortable with its Action-based architecture.
A Note on the Legacy laravel/ui
The laravel/ui package was the old standard for authentication. While it still works, it’s now considered a legacy tool.
The Tip: For any new project in 2025, you should use Breeze as the modern, simple alternative. Only use laravel/ui if you have a specific and strong reason to use the old Bootstrap framework it provides.
Quick Decision Guide
| Feature | Laravel Breeze | Laravel Jetstream |
| Purpose | A simple starter kit for authentication. | A full application starter kit. |
| Advanced Features | None | 2FA, Session Management, API Support, Teams |
| How to Customize | Edit controllers and views directly. | Edit published “Action” classes. |
| Best For | Projects needing full control and customization. | Projects needing advanced features out-of-the-box. |

Tip 3: Customizing User Registration Flows
Almost every Laravel application needs to change the default user registration process. You might need to add extra fields to the form or disable public sign-ups entirely.
Here are some pro tips on how to handle these common tasks the modern way in 2025, depending on whether you are using Laravel Breeze or Laravel Jetstream.
How to Add Custom Fields to Your Registration Form
Let’s say you want to add a phone_number field to your registration form. The process is slightly different for Breeze and Jetstream.
For Laravel Breeze:
With Breeze, the process is very direct and uses the standard MVC pattern.
- Update the Database: Add the phone_number column to your create_users_table migration file and run the migration.
- Update the View: Add the new input field for the phone number to the resources/views/auth/register.blade.php file.
- Update the Controller: Open the app/Http/Controllers/Auth/RegisteredUserController.php file. In the store method, add your new field to the validation rules and to the data array in the User::create() call.
For Laravel Jetstream:
The first step is the same, but the logic is in a different place.
- Update the Database and View: Add the phone_number column to your migration and the input field to your registration view, just like with Breeze.
- The Tip: Update the Action Class: Jetstream uses a “headless” backend. Instead of a controller, you need to find the app/Actions/Fortify/CreateNewUser.php file.
- Update the create Method: In that file, add your new field to the validation rules inside the Validator::make() call and to the data array in the User::create() call.
How to Disable Public Registration
For an admin panel or an invite-only application, you’ll need to turn off public sign-ups.
The Modern Tip: The correct and easiest way to do this in an app using Breeze or Jetstream is to edit your routes/auth.php file. Simply find the two routes that handle registration and comment them out or delete them.
PHP
// In routes/auth.php
// Route::get(‘register’, …)
// ->middleware(‘guest’)
// ->name(‘register’);
// Route::post(‘register’, …)
// ->middleware(‘guest’);
Now, anyone trying to go to the /register page will get a 404 Not Found error. For a cleaner project, you can also delete the RegisteredUserController.php and the register.blade.php view file, as they are no longer used.
How to Disable Auto-Login After Registration
Sometimes you want to force users to log in manually right after they register. With Breeze, this is a very simple change.
The Tip: In the app/Http/Controllers/Auth/RegisteredUserController.php file, find the store method. To disable the automatic login, just remove the Auth::login($user); line and change the redirect to point to the login page. You can even add a success message.
PHP
// …
// Auth::login($user); // This line is removed
return redirect(route(‘login’))->with(‘status’, ‘Registration successful. Please log in.’);
A Warning About Old Advice: Auth::routes()
You will see many old tutorials and Stack Overflow answers that tell you to disable registration by adding Auth::routes([‘register’ => false]); to your routes file.
Warning: This is outdated advice and will not work on a modern Laravel application that uses Breeze or Jetstream.
This is because modern starter kits create a physical routes/auth.php file where the routes are explicitly defined. The old helper method has no effect on this file. The correct way is always to edit the routes/auth.php file directly.
Tip 4: Advanced Login Customization
Many applications need more than Laravel’s default email and password login. You might want to let users log in with a username or add extra validation rules.
Here are some pro tips on how to customize your login flow the right way, whether you are using Laravel Breeze or Laravel Jetstream.
How to Allow Login with Username or Email
The default setup uses only email. Here’s how to let users log in with either their username or their email address.
Step 1: Database and View (For Both Breeze & Jetstream)
First, you need to make two simple changes:
- Add a username column to your users table using a new database migration.
- Update your resources/views/auth/login.blade.php file. Change the input field’s name attribute from email to something more general, like login, and update the label to “Username or Email.”
Step 2: The Logic (Different for Each Starter Kit)
This is where the tip depends on which starter kit you chose.
For Laravel Breeze: The tip is to customize the logic inside the LoginRequest.php file.
- Open app/Http/Requests/Auth/LoginRequest.php.
- In the authenticate() method, add logic to check if the user’s input is an email or a username. Then, build the correct credentials to pass to the Auth::attempt() method.
PHP
// In app/Http/Requests/Auth/LoginRequest.php
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$loginField = request()->input(‘login’);
// Check if input is an email or username
$fieldType = filter_var($loginField, FILTER_VALIDATE_EMAIL) ? ’email’ : ‘username’;
if (! Auth::attempt([$fieldType => $loginField, ‘password’ => $this->password], $this->boolean(‘remember’))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
‘login’ => trans(‘auth.failed’),
]);
}
RateLimiter::clear($this->throttleKey());
}
For Laravel Jetstream: With Jetstream, you don’t edit a controller. The tip is to hook into the Fortify service provider.
- Open your app/Providers/FortifyServiceProvider.php file.
- Inside the boot() method, use the Fortify::authenticateUsing() method. Here, you can write your own logic to find the user by either their email or username and then check their password.
PHP
// In app/Providers/FortifyServiceProvider.php
public function boot(): void
{
// …
Fortify::authenticateUsing(function (Request $request) {
$loginField = $request->input(config(‘fortify.username’));
$user = User::where(’email’, $loginField)
->orWhere(‘username’, $loginField)
->first();
if ($user && Hash::check($request->password, $user->password)) {
return $user;
}
});
}
How to Add Custom Login Validation
If you need to add extra validation rules to your login form (like a more complex password rule), the cleanest way is with a custom Form Request.
The Tip:
- Run the Artisan command php artisan make:request LoginRequest to create a new Form Request class.
In the new app/Http/Requests/LoginRequest.php file, add your custom rules to the rules() method.
PHP
// In app/Http/Requests/LoginRequest.php
public function rules(): array
{
return [
’email’ => [‘required’, ‘string’, ’email’],
‘password’ => [‘required’, ‘string’, ‘min:8’], // Example custom rule
];
}
- Finally, in your login controller (for Breeze, this is AuthenticatedSessionController), replace the standard Request with your new LoginRequest. Laravel will now automatically use your custom rules to validate every login attempt.
A final key takeaway is to know your starter kit’s architecture. Breeze is direct (you edit the controller and form request), while Jetstream is more abstract (you edit the service provider and action classes). Understanding this difference is the key to customizing your app without frustration.
Tip 5: Implementing Granular, Role-Based Login Redirects
A common task in many web applications is to send different types of users to different pages after they log in. For example, you might want to send an admin to an admin panel and a regular customer to their user dashboard.
With the release of Laravel 11, there is a new, much cleaner way to handle this. Here are the pro tips for setting up role-based login redirects.
The Modern Approach: Centralizing Redirects in bootstrap/app.php
The Tip: In Laravel 11 and newer, the best place to manage your application-wide login redirects is in the bootstrap/app.php file. This keeps your redirect logic in one central, easy-to-find place.
You can use the redirectUsersTo() method and give it a function that checks the user’s role and returns the correct route.
Here’s how you can send admins and customers to different dashboards:
PHP
// In bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
//…
->withMiddleware(function (Middleware $middleware) {
// This function handles redirects for logged-in users
$middleware->redirectUsersTo(function (Request $request) {
if ($request->user()->hasRole(‘admin’)) {
return route(‘admin.dashboard’);
}
if ($request->user()->hasRole(‘customer’)) {
return route(‘customer.dashboard’);
}
return route(‘dashboard’); // A default for everyone else
});
// This handles redirects for guests (not logged in)
$middleware->redirectGuestsTo(fn () => route(‘login’));
})
//…
->create();
Now, if a logged-in admin tries to visit the /login page, they will automatically be sent to the admin dashboard.
The Controller-Based Method for Specific Cases
Sometimes, you might need more specific control over the redirect from within your login controller.
The Tip: A clean way to handle this is to add a new method to your User model that determines the correct redirect route.
PHP
// In app/Models/User.php
public function getRedirectRoute(): string
{
return match((string) $this->role_name) {
‘admin’ => ‘admin.dashboard’,
‘editor’ => ‘editor.dashboard’,
default => ‘dashboard’,
};
}
Then, in your login controller (for example, Breeze’s AuthenticatedSessionController), you can call this method after the user is authenticated to send them to the right place.
PHP
// In a login controller’s store method
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
// Call the new method on the User model
return redirect()->intended(route(auth()->user()->getRedirectRoute()));
}
For Experienced Devs: A Guide to the New Changes in Laravel 11+
The Tip for Veterans: If you’ve used older versions of Laravel, you might be used to editing files like RouteServiceProvider.php or RedirectIfAuthenticated.php to change redirect logic. In Laravel 11 and newer, this has changed.
Those files are no longer part of the default application. All of this core application configuration is now done in the bootstrap/app.php file. This makes the configuration cleaner and more centralized, but it requires unlearning some old habits.
This table is a handy “cheat sheet” that shows where the old settings have moved in Laravel 11+.
| Task | Laravel 11+ Method (in bootstrap/app.php) | Legacy Location (Laravel <=10) |
| Redirect Guests | $middleware->redirectGuestsTo(‘/login’); | app/Http/Middleware/Authenticate.php |
| Redirect Logged-in Users | $middleware->redirectUsersTo(‘/dashboard’); | app/Http/Middleware/RedirectIfAuthenticated.php |
| Exclude from CSRF | $middleware->validateCsrfTokens(except: …) | app/Http/Middleware/VerifyCsrfToken.php |
| Add Global Middleware | $middleware->append(CustomMiddleware::class); | app/Http/Kernel.php |
Tip 6: Granular Route Control: Customizing and Disabling Authentication Routes
In modern Laravel, the old “magic” of automatically generated authentication routes is gone. Instead, starter kits like Laravel Breeze give you a physical file with all the routes written out plainly. This gives you full and direct control.
Here are some pro tips on how to use this new system to customize your app’s authentication.
The Key File: routes/auth.php
The Tip: When you install a starter kit like Laravel Breeze, it creates a new file at routes/auth.php. This file is your new central control panel for all authentication routes.
Inside, you will find a simple, readable list of all the routes for login, registration, password reset, and more. This makes it very easy to see and change exactly how your app’s authentication is structured.
How to Disable an Authentication Feature
If your application doesn’t need a feature like “Forgot Password,” you can disable it in seconds.
The Tip: Simply open the routes/auth.php file and comment out or delete the routes related to that feature. Once the routes are gone, the feature is disabled, and any links to it will result in a 404 error.
PHP
// In routes/auth.php
// — Comment out or delete these routes to disable password reset —
// Route::get(‘forgot-password’, …)->name(‘password.request’);
// Route::post(‘forgot-password’, …)->name(‘password.email’);
// Route::get(‘reset-password/{token}’, …)->name(‘password.reset’);
// Route::post(‘reset-password’, …)->name(‘password.update’);
How to Customize a Route’s URL
You can also easily change the URLs for your authentication pages, for example, changing /login to /signin.
The Tip: Open routes/auth.php and simply edit the URL string in the route definition. It’s a good idea to keep the route name() the same for compatibility with other parts of the framework.
PHP
// From this:
Route::get(‘login’, [AuthenticatedSessionController::class, ‘create’])
->middleware(‘guest’)
->name(‘login’);
// To this:
Route::get(‘signin’, [AuthenticatedSessionController::class, ‘create’])
->middleware(‘guest’)
->name(‘login’);
A Final Tip: Forget the Old Auth::routes() Method
The Tip for Veterans: If you have experience with older Laravel versions, you might remember using the Auth::routes() helper method in your routes/web.php file to manage authentication. This old method does not work with modern starter kits.
The new approach of having a physical routes/auth.php file is a major improvement. It’s a “what you see is what you get” system that gives you direct and clear control over your application’s routes without any hidden magic. The rule for 2025 is simple: to change your auth routes, edit the routes/auth.php file directly.
Tip 7: Fortifying Sensitive Actions with Password Confirmation
For important actions in your app, like changing billing details or deleting an account, you should make users re-enter their password to confirm it’s really them. This is an important security step that protects users if someone else gets access to their logged-in session, for example, on an unlocked computer.
Laravel has a built-in feature that makes this easy to implement.
How to Use the password.confirm Middleware
The Tip: The key to this feature is Laravel’s built-in password.confirm middleware. You can add it to any route that you want to protect to force the user to re-enter their password.
For example, to protect your billing settings page, you would simply add the middleware to the route definitions in your routes/web.php file:
PHP
// In routes/web.php
Route::get(‘/settings/billing’, function () {
// Page to show billing settings
})->middleware([‘auth’, ‘password.confirm’]);
Route::post(‘/settings/billing’, function () {
// Logic to update billing info
})->middleware([‘auth’, ‘password.confirm’]);
Now, any user who tries to visit /settings/billing will first be sent to a page where they must re-enter their password to continue.
How It Works Behind the Scenes
The process is simple and uses the user’s session to keep track of the confirmation. Here is the flow:
- Check: When a user visits a protected route, the middleware checks their session for a password_confirmed_at timestamp.
- Redirect: If the timestamp is missing or has expired, the user is redirected to the password confirmation page. The app saves the page they were originally trying to visit.
- Verify: The user enters their password, and the app checks to see if it’s correct.
- Confirm & Return: If the password is correct, a new timestamp is saved in the user’s session, and they are sent back to the page they were originally trying to visit.
Once confirmed, the user can visit any password-protected page without being asked again until the timer runs out.
How to Configure the Timeout
The Tip: You can easily change how long a password confirmation is valid before it expires.
Open the config/auth.php file and find the password_timeout option. This value is in seconds. The default is 10,800 seconds (3 hours). You can lower this number for higher security or raise it for more user convenience.
PHP
// In config/auth.php
‘password_timeout’ => 10800, // 3 hours in seconds
Jetstream’s Modal-Based Confirmation
For an enhanced user experience, especially for confirming single actions on a page rather than protecting an entire route, Laravel Jetstream offers a modal-based password confirmation flow.39 Instead of a full-page redirect, this approach displays a modal dialog overlaying the current page, asking the user to confirm their password. This is particularly useful for actions like enabling two-factor authentication or deleting a resource, as it keeps the user in the same context without the jarring effect of a page reload. This alternative demonstrates the flexibility of Laravel’s authentication ecosystem, providing different UX patterns to achieve the same security goal.
Ultimately, password confirmation should be viewed as an essential security pattern, not just an optional feature. It provides a crucial layered defense against session hijacking for any action that has significant financial, data integrity, or personal security implications. It moves the security posture beyond a simple binary check of “is the user logged in?” to a more nuanced and context-aware verification of user intent for critical operations.
Tip 8: Enhancing Security with Multi-Device Session Invalidation
Users access your application from many devices. If an account is compromised on one device, you need a way to secure it by logging them out everywhere else. Laravel has a powerful built-in feature for this.
Here are the pro tips on how to use it correctly.
How to Log Out Other Sessions
The Tip: The main tool for this feature is the Auth::logoutOtherDevices() method. To prevent misuse, it requires the user’s current password as an argument to confirm the request is legitimate.
Here is how you might use it in a controller right after a user successfully updates their password:
PHP
// In a controller method
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
public function updatePassword(Request $request)
{
// … your password validation logic …
// Verify the user’s current password is correct
if (! Hash::check($request->current_password, $request->user()->password)) {
return back()->withErrors([‘current_password’ => ‘The provided password does not match your current password.’]);
}
// Log out all other browser sessions
Auth::logoutOtherDevices($request->current_password);
// Update the user’s password
$request->user()->update([‘password’ => Hash::make($request->new_password)]);
return back()->with(‘status’, ‘Password updated and other sessions logged out.’);
}
Critical First Step: Enable the AuthenticateSession Middleware
The Tip: Auth::logoutOtherDevices() will fail silently if you don’t enable the Illuminate\Session\Middleware\AuthenticateSession middleware first. This is a step many developers miss.
To enable it in Laravel 11, first give it an alias in your bootstrap/app.php file.
PHP
// In bootstrap/app.php
return Application::configure(…)
->withMiddleware(function (Middleware $middleware) {
// Create an alias for the middleware
$middleware->alias([
‘auth.session’ => \Illuminate\Session\Middleware\AuthenticateSession::class,
]);
})
->create();
Then, apply that auth.session alias to your authenticated routes in routes/web.php.
PHP
// In routes/web.php
Route::middleware([‘auth’, ‘auth.session’])->group(function () {
// All routes that require authenticated users and session management
Route::get(‘/dashboard’, …);
Route::get(‘/profile’, …);
});
Best Times to Use This Feature
This feature is most effective in a few key situations:
- After a password change: This is the most common and highly recommended use. It ensures any potential attacker using the old password is immediately logged out.
- On a “Security Settings” page: Give users a button to “Log out from all other devices” so they can secure their account themselves.
- After suspicious activity: Your app can trigger it automatically if it detects a strange login.
The Most Important Tip for Production Apps
The Tip for Production: For this feature to work reliably on a live website with multiple servers (behind a load balancer), you must use a centralized session driver. The default file driver is not enough.
Here’s why: If your app is on two servers, Server A and Server B, each server keeps its own session files. A logout command on Server A won’t know about the session on Server B.
To fix this, you must change your session driver in your .env file or config/session.php to database or redis. This creates a single, shared place for all session data so the logoutOtherDevices() command works everywhere, every time.
A Bonus Tip for Sanctum API Tokens
The Tip for APIs: If you’re using Sanctum for API token-based authentication (for mobile apps, for example), you can achieve the same result by deleting all of a user’s other tokens.
PHP
// In a controller method for a Sanctum-authenticated route
$currentToken = $request->user()->currentAccessToken();
// Delete all tokens except the one making this request
$request->user()->tokens()->where(‘id’, ‘!=’, $currentToken->id)->delete();
Tip 9: Mitigating Brute-Force Attacks via Login Throttling
Brute-force attacks, where a hacker tries to guess a user’s password over and over, are a common threat. Laravel has a built-in feature called “login throttling” or “rate limiting” that automatically blocks these attempts.
Here are some pro tips on how to use and customize this important security feature.
How to Customize Login Throttling
The way you change the settings for login throttling depends on whether you are using Laravel Breeze or Laravel Jetstream.
For Laravel Breeze:
The Tip: With Breeze, you can change the throttling settings directly in the app/Http/Requests/Auth/LoginRequest.php file. ⚙️
- To change the maximum number of attempts (the default is 5), find the ensureIsNotRateLimited() method and change the number in the RateLimiter::tooManyAttempts() call.
- To change the lockout duration (the default is 60 seconds), find the authenticate() method and change the second number in the RateLimiter::hit() call.
For example, to change the limit to 3 attempts with a 2-minute (120 seconds) lockout:
PHP
// In app/Http/Requests/Auth/LoginRequest.php
public function authenticate(): void
{
// …
if (! Auth::attempt(…)) {
RateLimiter::hit($this->throttleKey(), 120); // Set lockout to 120 seconds
// …
}
}
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 3)) { // Set max attempts to 3
return;
}
// …
}
For Laravel Jetstream:
The Tip: With Jetstream, you configure throttling in the app/Providers/FortifyServiceProvider.php file.
Inside the boot() method, you will find a RateLimiter::for(‘login’, …) section. You can use the fluent Limit object to set a new rule. For example, to allow 3 attempts every 2 minutes:
PHP
// In app/Providers/FortifyServiceProvider.php
public function boot(): void
{
// …
RateLimiter::for(‘login’, function (Request $request) {
$email = (string) $request->input(’email’);
// Allow 3 attempts every 2 minutes, tracked by email and IP
return Limit::perMinutes(2, 3)->by($email . $request->ip());
});
}
An Important Security Tip: Understand the throttleKey
The Tip: By default, Laravel tracks failed login attempts using a combination of the user’s email AND their IP address. This is a very important security feature, and you should be very careful before changing it.
Here’s why the default is so smart:
- If it only tracked by IP address, an attacker could use thousands of different IPs to attack a single user account without being blocked.
- If it only tracked by email, an attacker could lock a real user out of their own account by intentionally failing to log in as them from a different computer.
Laravel’s default key (email|ip_address) solves both of these problems. It ensures that the rate limit is specific to one user trying to log in from one IP address. Changing this default can make your application vulnerable to these attacks.
Tip 10: A Synthesis of Security Best Practices for 2025
Good application security isn’t about one single feature; it’s about using multiple layers of defense that work together. This final tip is a checklist of the most important security practices for any modern Laravel application in 2025.
Essential Security Practices for Every App
These practices are the foundation of a secure application and should be standard on all your projects.
- Enforce CSRF Protection: Every form in your app that changes data (using POST, PUT, etc.) must be protected from Cross-Site Request Forgery. Laravel handles this automatically, but you must remember to add the @csrf directive to all your HTML forms.
- Use Strong Password Hashing: Never store passwords as plain text. Always use Laravel’s Hash facade. Use Hash::make() when creating a password and Hash::check() to verify it during login. This makes passwords nearly impossible to crack even if your database is stolen.
- Validate All User Input: You must validate all data that comes from users, whether it’s from a form or an API request. Laravel’s Form Requests are the best way to define and manage these rules. Good validation is your main defense against attacks like SQL injection and XSS.
- Update Your Dependencies Regularly: Many security holes come from outdated third-party packages. Run composer update regularly to get the latest security patches for all your project’s dependencies. Neglecting this leaves your app open to known attacks.
Advanced Security for Sensitive Applications
For applications that handle sensitive personal or financial data, these practices should be considered essential.
- Mandate Multi-Factor Authentication (MFA): In 2025, MFA is a standard security feature, not an optional extra. It provides a critical second layer of defense that protects user accounts even if a password is stolen. Laravel Jetstream provides a great 2FA system right out of the box.
- Configure Secure Sessions: Review your config/session.php file. Make sure the ‘secure’ option is set to true to only send cookies over HTTPS. For any app that runs on more than one server, you must change the session driver from file to a centralized store like database or redis.
- Use a Content Security Policy (CSP): A CSP is a browser-level security feature that provides an extra layer of defense against XSS attacks. You can set it up using middleware in Laravel. It works by telling the browser to only load scripts, styles, and images from sources you have specifically approved.
The Final Tip: Security is About Layers
Remember that good security is a system where different layers work together to protect your application.
Here is how the tips in this guide connect to create a strong defense:
- Login throttling (Tip 9) slows down online brute-force attacks.
- Strong password hashing makes offline attacks on a stolen database almost useless.
- MFA makes a stolen password useless on its own.
- Password confirmation (Tip 7) protects sensitive actions even if a user’s logged-in session is hijacked.
- Multi-device logout (Tip 8) gives users a way to regain control of their account after they detect a breach.
By using these principles together, you can build Laravel applications that are not just functional and fast, but also secure by design.
Conclusion
Securing your Laravel application requires a layered defense strategy.
Modern Laravel gives you powerful tools to protect user data. You start with a framework like Breeze or Jetstream. Then, you add security layers like login throttling, multi-factor authentication, and secure session management. When these features work together correctly, they create an application that is secure by design.
This approach ensures your project is not only functional but also resilient.
Review your application’s security layers. Contact our team for a complete Laravel authentication audit.