Laravel CSRF Protection Explained – Tokens, Middleware & AJAX Guide

Himmat Regar 1 Jun 14, 2025, 6:17 AM
Laravel
Views 580
Blog Thumbnail

1. What is CSRF?

Cross-Site Request Forgery (CSRF) is an attack that tricks a logged-in user’s browser into sending unwanted requests to your application.
If you don’t defend against it, an attacker can:

  • change a user’s email

  • transfer money

  • delete records

  • …anything the victim is authorised to do

The trick? The malicious request comes from the victim’s own browser, so it passes session authentication unless you have an extra “secret” check.


2. How Laravel solves the problem

Laravel’s built-in middleware VerifyCsrfToken adds a unique, random token to every active session:

  1. Token generated and stored in the session.

  2. The token is embedded into every HTML <form> you render with the @csrf Blade directive (or the helper csrf_field()).

  3. On POST, PUT, PATCH, or DELETE requests, the middleware compares the submitted token to the one saved in the session.

  4. If they match ⇒ request continues.
    If they don’t ⇒ 419 | Page Expired (token mismatch).

Good news: for typical Blade forms you get 100 % protection by default—just remember @csrf.


3. Quick example – Classic Blade form

Route & controller

// routes/web.php
Route::post('/profile/email', [ProfileController::class, 'updateEmail'])
     ->name('profile.email');

 

 
 
// app/Http/Controllers/ProfileController.php
public function updateEmail(Request $request)
{
    $request->validate([
        'email' => ['required', 'email'],
    ]);

    $request->user()->update([
        'email' => $request->email,
    ]);

    return back()->with('success', 'Email updated!');
}

 

View

<!-- resources/views/profile/email.blade.php -->
<form action="{{ route('profile.email') }}" method="POST">
    @csrf               <!-- 👈 inserts hidden _token input -->
    <label>Email</label>
    <input type="email" name="email" value="{{ old('email', auth()->user()->email) }}">
    <button type="submit">Save</button>
</form>

 

 

No further work needed—the CSRF middleware will verify the token automatically.


4. CSRF with AJAX or SPA front-ends

When you post via JavaScript you must send the token manually.
Laravel exposes it in a cookie named XSRF-TOKEN and in Blade you can also print it into a meta tag:

<meta name="csrf-token" content="{{ csrf_token() }}">

 

 

Axios example

import axios from 'axios';

// 1) read from meta tag
axios.defaults.headers.common['X-CSRF-TOKEN'] =
    document.querySelector('meta[name="csrf-token"]').getAttribute('content');

// 2) normal request
axios.post('/profile/email', { email: 'new@example.com' })
     .then(res => console.log(res.data))
     .catch(err => console.error(err));

 

 

Why not fetch from the XSRF-TOKEN cookie?
Browsers block JavaScript from reading http-only cookies.
Hence the Blade meta approach, or pull it from the DOM with @json(csrf_token()).


5. Excluding (or including) URIs

Sometimes you need to disable CSRF for specific endpoints—e.g. a public webhook.

Edit app/Http/Middleware/VerifyCsrfToken.php:

protected $except = [
    '/stripe/webhook',
    'https://trusted-service.com/*',
];

 

 

Conversely, if you’re building an API that uses tokens (Laravel Sanctum, Passport, etc.) you’ll often remove the CSRF middleware from your api middleware group entirely.


6. Regenerating tokens after login / logout

Laravel automatically regenerates the session ID and CSRF token when:

  • a user logs in (LoginController)

  • a user logs out (LogoutController)

This prevents session fixation and token reuse attacks.


7. Common pitfalls & fixes

Issue Cause Fix
**419 Page Expired** on POST Token missing or wrong
Token mismatch only in Firefox or Safari Mixed domain/sub-domain cookies Set identical domain on session cookie or disable SESSION_DOMAIN in .env
Testing fails with 419 Token not included in test In Feature tests, call withSession(['_token' => csrf_token()]) or just use $response = $this->post(route(...), $data) which automatically sets it
SPA with Inertia / Livewire Token out of sync after login via XHR Refresh token from cookie/meta after axios.post('/login') succeeds

8. Writing Feature tests with CSRF

public function test_email_update_is_protected()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    // Send request WITHOUT token
    $response = $this->post('/profile/email', [
        'email' => 'evil@example.com',
    ]);

    $response->assertStatus(419);
}

public function test_email_update_success()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    // Send request WITH token (auto-added)
    $response = $this->post('/profile/email', [
        'email' => 'new@example.com',
        '_token' => csrf_token(),
    ]);

    $response->assertRedirect();
    $this->assertEquals('new@example.com', $user->fresh()->email);
}

 

 

Laravel’s test helpers handle tokens automatically unless you pass a custom $headers array—then you must include it yourself.


9. Advanced: Changing the default header name

If you prefer another header (e.g. X-XSRF-TOKEN instead of X-CSRF-TOKEN) add in AppServiceProvider:

use Illuminate\Support\Facades\URL;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

public function boot()
{
    Middleware::$header = 'x-xsrf-token';
}

 

 

10. Key take-aways

  • Blade forms: always include @csrf.

  • JS/AJAX: send the token via X-CSRF-TOKEN header.

  • Don’t disable CSRF unless you have a very good reason.

  • Regenerate tokens on sensitive auth events.

  • Test both protected & unprotected flows.


✨ That’s it! You now have a full understanding of Laravel’s CSRF protection and how to use it safely in traditional Blade apps, SPAs, and APIs.

💡 Frequently Asked Questions (FAQ) about Laravel CSRF Protection

# Question Short Answer
1 What exactly is the _token field that Blade inserts? A hidden form input containing the CSRF token stored in the user’s session. Laravel’s middleware checks it on every state-changing request.
2 Do I need CSRF tokens on GET routes? No. Only methods that modify state (POST, PUT, PATCH, DELETE) are checked. GET requests should remain idempotent, so CSRF is not validated there.
3 **Why do I get “419 Page Expired” even though I added @csrf?**
4 How do I send the token in Axios or Fetch? Read it once from the <meta name="csrf-token"> tag and set it on every request header: axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content;.
5 What’s the difference between X-CSRF-TOKEN and X-XSRF-TOKEN? X-CSRF-TOKEN is what you set in JS. XSRF-TOKEN is a cookie Laravel sets so that frameworks like Angular or Axios can automatically read and forward it (if you enable that feature).
6 Can I disable CSRF for a single webhook URL? Yes. Add the URI to the $except array inside app/Http/Middleware/VerifyCsrfToken.
7 Is CSRF protection required for APIs that use Sanctum or Passport? If you’re using token-based auth (the mobile-app style), you usually remove the CSRF middleware from api routes. If you use Sanctum’s SPA mode (cookie-based), keep CSRF enabled.
8 Does Livewire handle CSRF for me? Yes. Livewire’s request payload automatically includes the token, so no manual work is needed.
9 Should tokens be regenerated on every request? No. Laravel regenerates the token on login/logout and when the session ID changes. That’s sufficient; regenerating every request would break concurrent tabs.
10 How do I test CSRF-protected routes in PHPUnit? Use the built-in HTTP helpers ($this->post(...)) — they automatically attach a valid token when the test is run inside a session established with actingAs().

Still stuck?

  • Debug tip: Dump the current token in your view ({{ csrf_token() }}) and compare it with the one arriving in Request::header('x-csrf-token').

  • Local vs production: On localhost you can set appVerificationDisabledForTesting, but never disable CSRF middleware in production.

 

Related Posts

understanding-laravel-models
149 viewsLaravel
Himmat Kumar • Jun 14, 2025, 4:34 AM

Understanding Laravel Models: A Comprehensive Guide

laravel-setup-tutorial
185 viewsLaravel
Himmat Kumar • Jun 14, 2025, 4:23 AM

Master Laravel: Basic System Requirement,Installation a...

laravel-service-container-guide
261 viewsLaravel
Himmat Regar 1 • Jun 14, 2025, 3:57 AM

Mastering Laravel Service Container: Dependency Injecti...

laravel-request-guide
657 viewsLaravel
Himmat Regar 1 • Jun 14, 2025, 3:46 AM

Laravel Request: Input, Validation & Tips (Guide 2025)

What-is-laravel-controller
150 viewsLaravel
Himmat Kumar • Jun 14, 2025, 3:40 AM

What is Laravel - Controllers

what-is-laravel-request
174 viewsLaravel
Himmat Kumar • Jun 14, 2025, 3:30 AM

What is a request in Laravel?

mastering-laravel-middleware
260 viewsLaravel
Himmat Regar 1 • Jun 14, 2025, 3:24 AM

Mastering Laravel Middleware: Registration, Customizati...

laravel-cookies-guide
575 viewsLaravel
Himmat Regar 1 • Jun 14, 2025, 3:23 AM

Laravel Cookies: Secure, Encrypted & Easy (Guide 2025)

what-is-laravel-routing
112 viewsLaravel
Himmat Kumar • Jun 14, 2025, 2:29 AM

What is laravel routing

get-route-in-laravel
106 viewsLaravel
Himmat Kumar • Jun 14, 2025, 1:32 AM

GET Route in Laravel