Authentication answers “who are you.” Authorization answers “what are you allowed to do.” Most Laravel security incidents I review are failures of the second, not the first: the login works perfectly, and then any logged-in user can read any other user’s data by changing an ID in the URL. This guide covers both halves, in the order I check them during a review.
It is one of the deeper guides linked from the Laravel Security Review hub. If you want the fast pre-deploy pass first, start with the Laravel security checklist for production.
Choose the right authentication tool
Laravel gives you three first-party options, and picking the wrong one creates work and risk later.
- Sanctum for most apps: session auth for your own SPA or Blade frontend, and simple API tokens for mobile clients and scripts. It is the right default.
- Passport only when you genuinely need full OAuth2, for example when third parties build against your API on behalf of your users.
- Fortify as the backend for login, registration, password reset and two-factor, when you want to own the frontend.
Reaching for Passport when Sanctum would do is the most common over-engineering I see. More moving parts means more to secure.
Get the password fundamentals right
Laravel hashes passwords with bcrypt by default, and argon2id is available. Both are correct choices. The failures come from around them:
- Never log the password field. Add it to
$hiddenon the model and exclude it from request logging. - Enforce strength at validation time with the
Passwordrule, including a check against known breached passwords:
use Illuminate\Validation\Rules\Password;
$request->validate([
'password' => ['required', 'confirmed', Password::min(12)->uncompromised()],
]);
Throttle and slow down attackers
A login form with no rate limit is an open invitation to credential stuffing. This is the same class of attack that fills server logs with thousands of failed attempts against real sites.
Route::post('/login', [LoginController::class, 'store'])
->middleware('throttle:5,1'); // 5 attempts per minute per IP
Pair the throttle with account lockout after repeated failures, and log every failure so a distributed attempt is visible even when no single IP looks busy.
Verify email and offer two-factor
Implement the MustVerifyEmail contract so unverified accounts cannot act, and gate sensitive routes behind the verified middleware. For anything handling money or personal data, offer two-factor authentication; Fortify ships it, and it turns a stolen password from a breach into a dead end.
Authorize every action, not just the route
This is where the real risk sits. Authenticating a user tells you they are logged in. It says nothing about whether this user may touch this record. Laravel gives you Gates and Policies for exactly this, and the mistake is not using them.
Policies for models
// app/Policies/PostPolicy.php
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
// in the controller
public function update(Request $request, Post $post)
{
$this->authorize('update', $post); // throws 403 if not allowed
$post->update($request->validated());
}
The authorize() call is the line that stops one user editing another user’s post by guessing its ID. Without it, the route is authenticated but not authorized, and that gap is the most common broken-access-control bug in the wild.
Gates for abilities
Gate::define('view-admin', fn (User $user) => $user->is_admin);
Use Gates for coarse abilities that are not tied to a specific model, and Policies for per-record decisions. In Blade, hide UI the user cannot use with @can('update', $post), but never rely on hidden UI as the control. The server-side authorize() is the control; the hidden button is a courtesy.
Protect the session itself
- Regenerate the session on login (
$request->session()->regenerate()) to prevent session fixation. - Set
SESSION_SECURE_COOKIE=true,SESSION_HTTP_ONLY=trueand an appropriateSESSION_SAME_SITE. - Invalidate other sessions on password change with
Auth::logoutOtherDevices().
The token concept underneath sessions and CSRF protection is worth understanding in its own right; see Why do we use a token concept in PHP?.
Related
- Laravel Security Review: a practical walk through reviewing and hardening a Laravel application, area by area, with every guide in this cluster in one place.
- A Laravel Security Checklist for Production
- Preventing the OWASP Top 10 in Laravel
- 7 Laravel Features to Enhance the Security of Your Application
- Why do we use a token concept in PHP?