CSRF Token Mismatch di Laravel: Penyebab dan Solusi

CSRF token mismatch di Laravel berarti request yang mengubah data tidak membawa token CSRF yang cocok dengan session. Jawaban cepatnya: pastikan form punya @csrf, AJAX mengirim header token, cookie/session ikut terkirim, dan domain aplikasi tidak berubah-ubah.

Error ini dekat dengan 419 Page Expired, tetapi lebih spesifik. Laravel menolak request karena token yang diterima kosong, salah, sudah kedaluwarsa, atau berasal dari session yang berbeda.

Kapan Masalah Ini Biasanya Muncul?

  • submit form POST, PUT, PATCH, atau DELETE gagal
  • response berisi CSRF token mismatch
  • request AJAX aman di Postman, tetapi gagal di browser
  • login SPA gagal setelah memanggil endpoint Laravel
  • form delete di Blade tidak bekerja
  • error muncul setelah pindah domain, port, atau subdomain

Penyebab Paling Umum

1. Form Tidak Memakai @csrf

Form yang mengubah data wajib menyertakan token.

<form method="POST" action="/produk">
    <input type="text" name="nama">
    <button type="submit">Simpan</button>
</form>

Form di atas belum punya token, jadi Laravel akan menolak request.

2. AJAX Tidak Mengirim Header Token

Kalau request dibuat dengan fetch, Axios, atau frontend sendiri, token harus ikut dikirim lewat header.

Token CSRF terkait dengan session. Kalau browser tidak mengirim cookie session, Laravel tidak bisa mencocokkan token.

Ini sering terjadi pada frontend dan backend yang beda domain atau beda port.

4. Session Kedaluwarsa

User membuka form terlalu lama. Saat submit, token lama sudah tidak cocok dengan session aktif.

5. Domain atau Config Session Tidak Konsisten

Contoh umum:

  • kadang akses localhost, kadang 127.0.0.1
  • kadang pakai http, kadang https
  • SESSION_DOMAIN salah
  • SPA beda subdomain tetapi cookie tidak dikirim

Langkah Diagnosis

  1. Cek apakah route ada di routes/web.php dan memakai middleware web.
  2. Pastikan form Blade punya @csrf.
  3. Buka browser devtools, cek request payload atau headers.
  4. Pastikan cookie session Laravel ikut terkirim.
  5. Kalau memakai AJAX, cek header X-CSRF-TOKEN atau X-XSRF-TOKEN.
  6. Pastikan domain yang dipakai konsisten.
  7. Jalankan php artisan optimize:clear setelah mengubah config session.
php artisan optimize:clear
Cek Cookie dan Header Bersamaan

CSRF bukan hanya soal token di form. Token harus cocok dengan session. Jadi cek token dan cookie session dalam request yang sama.

Langkah Fix

1. Tambahkan @csrf di Form Blade

<form method="POST" action="{{ route('produk.store') }}">
    @csrf

    <label for="nama">Nama Produk</label>
    <input id="nama" type="text" name="nama">

    <button type="submit">Simpan</button>
</form>

Untuk form update dan delete, tetap pakai @csrf.

<form method="POST" action="{{ route('produk.destroy', $produk) }}">
    @csrf
    @method('DELETE')

    <button type="submit">Hapus</button>
</form>

2. Kirim Token untuk Request fetch

Tambahkan meta tag di layout utama:

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

Lalu kirim header saat request:

const token = document
  .querySelector('meta[name="csrf-token"]')
  .getAttribute('content');

await fetch('/produk', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
    'X-CSRF-TOKEN': token,
  },
  body: JSON.stringify({
    nama: 'Kaos Polos',
  }),
});

Kalau frontend beda origin, request harus mengirim credential.

await fetch('http://localhost:8000/produk', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-TOKEN': token,
  },
  body: JSON.stringify({ nama: 'Kaos Polos' }),
});

Untuk SPA yang memakai Sanctum, biasanya alurnya dimulai dari mengambil cookie CSRF.

await fetch('http://localhost:8000/sanctum/csrf-cookie', {
  credentials: 'include',
});

4. Konsisten Memakai Satu Domain

Pilih satu alamat saat development:

http://toko-keren.test

Jangan bergantian dengan:

http://localhost:8000
http://127.0.0.1:8000

Kalau domain berubah, cookie session bisa dianggap berbeda.

5. Jangan Menonaktifkan CSRF untuk Route Web Biasa

Menonaktifkan CSRF memang membuat error hilang, tetapi membuka celah keamanan. Untuk form web biasa, perbaiki token dan session-nya.

Contoh Sebelum dan Sesudah

Sebelum

<form method="POST" action="/logout">
    <button type="submit">Logout</button>
</form>

Route logout biasanya POST, jadi form ini bisa gagal karena tidak ada token.

Sesudah

<form method="POST" action="/logout">
    @csrf
    <button type="submit">Logout</button>
</form>

Kesalahan yang Sering Terjadi

Mengira Semua Request API Butuh CSRF

API token Bearer biasanya tidak memakai CSRF. CSRF relevan untuk request berbasis session/cookie, terutama route web dan SPA first-party.

Lupa credentials: 'include' Saat Frontend Beda Origin

Token ada, tetapi cookie session tidak dikirim. Hasilnya tetap mismatch.

Mengambil Token dari Halaman Lama

Kalau tab dibiarkan terbuka sangat lama, token bisa tidak cocok lagi. Refresh halaman atau minta token baru.

Mematikan Middleware CSRF

Ini sering dilakukan saat panik. Untuk project belajar pun sebaiknya biasakan memperbaiki penyebabnya.

Pencegahan

  1. Selalu tulis @csrf setiap membuat form yang mengubah data.
  2. Simpan meta CSRF token di layout utama.
  3. Gunakan satu domain yang konsisten saat development.
  4. Untuk SPA, dokumentasikan flow cookie dan CSRF.
  5. Jangan mencampur token API Bearer dengan flow session tanpa alasan jelas.

Bacaan Terkait

FAQ

Apa bedanya CSRF token mismatch dan 419 Page Expired?

CSRF token mismatch adalah penyebab spesifiknya. 419 Page Expired sering menjadi response Laravel saat token CSRF atau session tidak valid.

Apakah semua form Laravel wajib @csrf?

Form POST, PUT, PATCH, dan DELETE wajib memakai CSRF. Form GET untuk pencarian atau filter biasanya tidak perlu.

Kenapa request berhasil di Postman tetapi gagal di browser?

Postman tidak selalu mengikuti aturan cookie, CORS, dan credential browser. Kalau gagal hanya di browser, cek cookie session, origin, dan header CSRF.

Apakah boleh exclude route dari CSRF?

Boleh untuk kasus khusus seperti webhook dari layanan eksternal, tetapi jangan untuk form web biasa atau route login.