Jika Anda membangun SPA dengan backend Laravel dan frontend di subdomain berbeda, cookie session sering menjadi pilihan yang lebih aman dan lebih sederhana dibanding menyimpan token di JavaScript. Namun, agar pendekatan ini benar-benar aman, Anda harus memahami cara kerja SameSite, Secure, HttpOnly, domain cookie, CSRF, CORS, dan regenerasi session setelah login.
Artikel ini membahas cara menerapkan cookie session aman untuk SPA dan subdomain di Laravel secara praktis. Fokusnya bukan hanya agar login berhasil, tetapi juga agar arsitekturnya tahan terhadap kesalahan umum seperti cookie tidak terkirim, CSRF gagal, session fixation, dan kebocoran cookie di production.
Kapan memilih session cookie dibanding token
Untuk SPA, ada dua pendekatan umum:
- Session cookie: browser menyimpan cookie, lalu otomatis mengirimkannya ke server pada request yang cocok dengan domain, path, dan aturan keamanan cookie.
- Token: frontend menyimpan token, lalu mengirimkannya secara eksplisit di header seperti
Authorization: Bearer ....
Kelebihan session cookie
- HttpOnly dapat mencegah JavaScript membaca session cookie secara langsung.
- Rotasi dan invalidasi session biasanya lebih mudah dikelola di server.
- Lebih cocok untuk autentikasi stateful dengan proteksi CSRF yang jelas.
- Frontend tidak perlu mengelola token sensitif di localStorage atau sessionStorage.
Kelebihan token
- Cocok untuk arsitektur stateless atau integrasi dengan banyak klien non-browser.
- Lebih fleksibel untuk API publik atau komunikasi antar layanan.
- Tidak terlalu bergantung pada mekanisme cookie browser.
Kapan session cookie lebih tepat
Pilih session cookie jika:
- Frontend Anda adalah browser-based SPA.
- Backend dan frontend masih dalam satu kelompok domain, misalnya
app.example.comdanapi.example.com. - Anda ingin mengurangi risiko token bocor lewat JavaScript.
- Anda siap mengonfigurasi CSRF dan CORS dengan benar.
Untuk banyak kasus SPA internal atau produk web biasa, pendekatan ini adalah pilihan yang kuat selama konfigurasi cookie dan request stateful tidak salah.
Arsitektur dasar SPA + Laravel pada multi-subdomain
Contoh skenario:
- Frontend SPA:
https://app.example.com - Backend Laravel API:
https://api.example.com
Alur sederhananya:
- Browser membuka SPA di
app.example.com. - SPA meminta endpoint awal untuk mendapatkan cookie CSRF jika diperlukan.
- SPA mengirim request login ke
api.example.comdengancredentialsaktif. - Laravel memverifikasi kredensial, membuat session, lalu mengirim session cookie.
- Untuk request berikutnya, browser otomatis mengirim session cookie ke backend.
- Request yang mengubah state tetap membawa proteksi CSRF.
Hal pentingnya: frontend tidak perlu membaca isi session cookie. Browser yang akan mengelolanya, selama domain, protokol HTTPS, dan aturan CORS sesuai.
Konfigurasi cookie session aman di Laravel
Contoh .env
Nilai berikut adalah contoh realistis yang umum untuk deployment HTTPS dengan frontend dan API pada subdomain yang sama-sama berada di bawah satu domain induk:
APP_URL=https://api.example.com
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=.example.com
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
SANCTUM_STATEFUL_DOMAINS=app.example.com
CORS_ALLOWED_ORIGINS=https://app.example.comCatatan penting:
SESSION_DOMAIN=.example.commemungkinkan cookie berlaku lintas subdomain sepertiapp.example.comdanapi.example.com.SESSION_SECURE_COOKIE=trueharus digunakan di production agar cookie hanya dikirim melalui HTTPS.SESSION_SAME_SITE=laxumumnya cocok jika SPA dan API masih satu situs secara konsep domain. Jika Anda benar-benar perlu cookie pada konteks cross-site yang lebih ketat, Anda mungkin perlunone, tetapi itu mensyaratkanSecure.SESSION_DRIVERsebaiknya memakai backend yang layak untuk production seperti Redis atau database, bukan file untuk deployment yang tersebar di banyak instance.
Contoh config/session.php
Pastikan konfigurasi session mengacu ke environment dan mendukung keamanan dasar:
<?php
return [
'driver' => env('SESSION_DRIVER', 'file'),
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => false,
'encrypt' => env('SESSION_ENCRYPT', true),
'files' => storage_path('framework/sessions'),
'connection' => env('SESSION_CONNECTION'),
'table' => 'sessions',
'store' => env('SESSION_STORE'),
'lottery' => [2, 100],
'cookie' => env('SESSION_COOKIE', 'laravel_session'),
'path' => env('SESSION_PATH', '/'),
'domain' => env('SESSION_DOMAIN'),
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => env('SESSION_SAME_SITE', 'lax'),
];Poin penting:
- HttpOnly sebaiknya tetap
trueagar session cookie tidak dapat dibaca JavaScript. - Encrypt memberi lapisan tambahan untuk isi cookie yang memang perlu dienkripsi oleh mekanisme Laravel.
- Secure wajib di production berbasis HTTPS.
Memahami SameSite untuk SPA dan subdomain
SameSite mengontrol kapan browser boleh mengirim cookie pada request lintas konteks.
- Lax: aman sebagai default untuk banyak aplikasi yang masih berada dalam satu grup domain dan tidak membutuhkan pola embed atau cross-site yang rumit.
- Strict: lebih ketat, tetapi sering membuat alur login atau perpindahan halaman tertentu menjadi tidak nyaman.
- None: dipakai jika cookie memang harus dikirim dalam konteks cross-site; harus disertai Secure.
Kesalahan umum adalah langsung memakai SameSite=None tanpa benar-benar membutuhkannya. Jika frontend dan backend masih berada pada subdomain dari domain yang sama, mulai dari Lax biasanya lebih aman dan lebih sederhana.
Jika cookie tidak terkirim, jangan langsung menyimpulkan bahwa masalahnya ada di Laravel. Sering kali akar masalah ada di kombinasi browser policy, HTTPS, CORS, atau salah menentukan domain cookie.
CSRF untuk request stateful pada SPA
Jika Anda memakai session cookie, maka request yang mengubah state seperti POST, PUT, PATCH, dan DELETE harus diproteksi dari CSRF. Ini karena browser bisa mengirim cookie otomatis.
Mengapa CSRF tetap wajib
Session cookie yang otomatis terkirim adalah kelebihan sekaligus risiko. Tanpa proteksi CSRF, situs lain dapat mencoba memancing browser korban mengirim request ke backend Anda dengan cookie yang valid.
Alur umum CSRF untuk SPA
- SPA memanggil endpoint awal untuk mendapatkan cookie CSRF.
- Browser menyimpan cookie tersebut.
- Frontend mengirim request login atau request mutasi lain dengan
credentials: 'include'. - Header token CSRF dikirim sesuai mekanisme yang dipakai aplikasi Anda.
Contoh request dari frontend:
await fetch('https://api.example.com/sanctum/csrf-cookie', {
method: 'GET',
credentials: 'include'
});
await fetch('https://api.example.com/login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
email: '[email protected]',
password: 'secret'
})
});Jika Anda menggunakan klien HTTP seperti Axios, pastikan pengiriman cookie lintas subdomain diaktifkan. Jika menggunakan fetch, gunakan credentials: 'include'.
CORS yang aman untuk SPA
CORS tidak menggantikan autentikasi. Fungsinya adalah memberi tahu browser origin mana yang diizinkan mengakses respons dari backend. Jika salah konfigurasi, cookie bisa gagal terkirim atau, lebih buruk, origin yang tidak seharusnya bisa diizinkan.
Contoh config/cors.php
<?php
return [
'paths' => [
'api/*',
'login',
'logout',
'sanctum/csrf-cookie',
'user',
],
'allowed_methods' => ['*'],
'allowed_origins' => array_filter([
env('CORS_ALLOWED_ORIGINS'),
]),
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];Contoh .env jika hanya satu origin frontend yang boleh mengakses:
CORS_ALLOWED_ORIGINS=https://app.example.comPraktik penting:
- Jangan gunakan
*padaallowed_originsjikasupports_credentialsaktif. - Batasi hanya origin frontend yang benar-benar diperlukan.
- Pastikan frontend selalu mengirim request dengan credential aktif jika endpoint membutuhkan session.
Kesalahan umum pada CORS
- Mengizinkan semua origin saat cookie credential aktif.
- Lupa menambahkan endpoint login atau CSRF ke path CORS.
- Origin frontend memakai
httptetapi backend memaksa cookie secure. - Frontend dan backend berbeda port saat development, tetapi origin yang diizinkan tidak diperbarui.
Alur login SPA yang benar di Laravel
Contoh controller login
Setelah autentikasi berhasil, lakukan regenerasi session untuk mencegah session fixation.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class AuthenticatedSessionController extends Controller
{
public function store(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
throw ValidationException::withMessages([
'email' => ['Kredensial tidak valid.'],
]);
}
$request->session()->regenerate();
return response()->json([
'message' => 'Login berhasil.',
'user' => $request->user(),
]);
}
public function destroy(Request $request)
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json([
'message' => 'Logout berhasil.',
]);
}
}Mengapa urutannya penting:
Auth::attempt()memverifikasi kredensial dan mengautentikasi user.session()->regenerate()membuat session ID baru setelah login untuk mengurangi risiko session fixation.- Saat logout,
invalidate()menghapus session lama danregenerateToken()memperbarui token CSRF.
Contoh route yang menggunakan middleware autentikasi
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
->middleware('guest');
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
->middleware('auth');
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth');Session lifetime, rotasi, dan logout semua device
Menentukan session lifetime
SESSION_LIFETIME adalah kompromi antara keamanan dan kenyamanan pengguna.
- Terlalu singkat: pengguna sering ter-logout.
- Terlalu panjang: jendela penyalahgunaan session menjadi lebih besar jika cookie bocor.
Untuk aplikasi administratif atau data sensitif, gunakan lifetime yang lebih pendek dan pertimbangkan timeout idle. Untuk aplikasi umum, nilai moderat sering lebih realistis.
Logout semua device
Jika kebutuhan Anda mencakup pengakhiran sesi di semua perangkat, jangan hanya menghapus cookie pada browser saat ini. Anda perlu menghapus atau menginvalidasi semua session user di sisi server.
Strategi umumnya:
- Simpan session di backend terpusat seperti database atau Redis.
- Hubungkan session dengan user yang login.
- Saat pengguna memilih “logout semua device”, hapus seluruh session aktif miliknya di store.
Implementasi detailnya bergantung pada driver session dan desain aplikasi, tetapi prinsipnya sama: invalidasi harus terjadi di server, bukan hanya di browser yang sedang aktif.
Trade-off driver session
- file: sederhana, kurang cocok untuk multi-instance production.
- database: mudah diaudit, lebih mudah untuk operasi hapus berdasarkan user jika struktur data mendukung.
- redis: cepat dan cocok untuk skala lebih tinggi, tetapi tetap perlu strategi invalidasi yang jelas.
Middleware dan kontrol request stateful
Selain route auth dan guest, pastikan grup middleware web atau stateful request Anda benar-benar aktif untuk endpoint yang memakai session dan CSRF. Jika Anda memisahkan route API dan route web, kesalahan umum adalah menaruh endpoint login berbasis session di grup middleware yang tidak memuat session sama sekali.
Contoh middleware kustom sederhana untuk memperketat origin
Ini bukan pengganti CORS, tetapi bisa menjadi lapisan validasi tambahan untuk endpoint sensitif jika Anda ingin memeriksa header Origin pada request browser:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureFrontendOrigin
{
public function handle(Request $request, Closure $next)
{
$allowed = [
'https://app.example.com',
];
$origin = $request->headers->get('Origin');
if ($origin && ! in_array($origin, $allowed, true)) {
abort(403, 'Origin tidak diizinkan.');
}
return $next($request);
}
}Gunakan dengan hati-hati. Header Origin berguna untuk request browser, tetapi bukan satu-satunya kontrol keamanan. Jangan menggantungkan autentikasi pada header ini.
Risiko utama: session fixation dan kebocoran cookie
Session fixation
Session fixation terjadi ketika attacker berhasil memaksa korban memakai session ID yang sudah diketahui attacker, lalu korban login menggunakan session tersebut. Karena itu, session ID harus diganti setelah login.
Pertahanan utamanya:
- Panggil
$request->session()->regenerate()setelah login berhasil. - Pastikan alur autentikasi tidak mempertahankan session ID lama secara tidak perlu.
Kebocoran cookie
Cookie bisa bocor karena beberapa sebab:
- Tidak memakai HTTPS sehingga cookie lewat koneksi tidak terenkripsi.
HttpOnlydimatikan dan ada XSS di frontend.- Domain cookie terlalu luas sehingga terkirim ke subdomain yang tidak dipercaya.
- Log, proxy, atau tooling development menampilkan header sensitif.
Pertahanannya:
- Aktifkan Secure di production.
- Pertahankan HttpOnly untuk session cookie.
- Batasi SESSION_DOMAIN hanya jika memang perlu lintas subdomain.
- Kurangi jumlah subdomain yang dapat dipercaya.
- Lindungi aplikasi dari XSS, karena XSS dapat menyalahgunakan sesi pengguna walau cookie HttpOnly tidak bisa dibaca langsung.
Kesalahan umum di production
- APP_URL, domain frontend, dan domain cookie tidak konsisten.
- Cookie diset untuk
.example.com, tetapi aplikasi sebenarnya berjalan di domain berbeda. SESSION_SECURE_COOKIE=truediaktifkan, tetapi ada request masih lewat HTTP.- Load balancer atau reverse proxy tidak meneruskan informasi HTTPS dengan benar, sehingga aplikasi salah mengira request tidak aman.
- Session driver
filedipakai pada banyak container atau instance tanpa shared storage. - Frontend lupa memakai
credentials: 'include'. - CORS terlalu longgar atau justru memblokir origin yang sah.
- Endpoint login ditempatkan di grup middleware yang tidak memulai session.
- Tidak ada regenerasi session setelah login.
- Logout hanya menghapus state frontend tanpa menginvalidasi session server.
Checklist hardening cookie session Laravel
- Gunakan HTTPS penuh di semua environment yang mendekati production.
- Set
SESSION_SECURE_COOKIE=true. - Pastikan
http_onlyaktif untuk session cookie. - Pilih
same_sitepaling ketat yang masih mendukung alur aplikasi, biasanya mulai darilax. - Set
SESSION_DOMAINhanya sejauh yang diperlukan. - Regenerasi session setelah login.
- Invalidate session dan regenerate CSRF token saat logout.
- Batasi origin pada CORS, jangan gunakan wildcard untuk request ber-credential.
- Pastikan request stateful dilindungi CSRF.
- Gunakan driver session yang cocok untuk arsitektur production, seperti Redis atau database.
- Audit subdomain lain yang berada di bawah domain yang sama.
- Hindari menyimpan token sensitif di localStorage jika Anda sudah memakai session cookie.
- Periksa header dan cookie di browser DevTools setelah deployment.
Strategi pengujian manual yang bisa langsung dilakukan
1. Verifikasi cookie saat login
- Buka DevTools browser.
- Masuk ke tab Network dan Application/Storage.
- Lakukan login dari SPA.
- Periksa apakah respons login atau endpoint awal mengatur cookie yang benar.
- Pastikan atribut penting terlihat sesuai kebutuhan: domain, path, Secure, dan HttpOnly.
2. Verifikasi request berikutnya benar-benar membawa session
- Setelah login, panggil endpoint seperti
/user. - Periksa bahwa browser mengirim cookie ke
api.example.com. - Pastikan respons mengembalikan user terautentikasi.
3. Uji CSRF
- Kirim request mutasi tanpa langkah CSRF yang benar.
- Pastikan server menolak request tersebut.
- Ulangi setelah alur CSRF benar, lalu pastikan request berhasil.
4. Uji CORS
- Coba panggil API dari origin yang sah.
- Pastikan browser mengizinkan respons.
- Coba dari origin yang tidak diizinkan.
- Pastikan request diblokir oleh kebijakan CORS atau kontrol tambahan server.
5. Uji logout
- Login lalu akses endpoint terproteksi.
- Lakukan logout.
- Coba akses endpoint terproteksi lagi.
- Pastikan akses gagal dan session lama tidak lagi berlaku.
6. Uji skenario fixation secara sederhana
- Catat session identifier sebelum login dari DevTools jika terlihat sebagai cookie session.
- Login.
- Pastikan session berubah setelah autentikasi berhasil.
7. Uji deployment di balik proxy
- Verifikasi bahwa aplikasi mengenali request sebagai HTTPS.
- Pastikan cookie Secure tetap terkirim setelah melewati reverse proxy atau CDN.
Rekomendasi implementasi yang aman dan realistis
Untuk mayoritas aplikasi Laravel dengan SPA di subdomain terpisah, konfigurasi yang biasanya masuk akal adalah:
- Session berbasis cookie dengan HttpOnly dan Secure.
SESSION_DOMAIN=.example.comhanya jika memang perlu berbagi session lintas subdomain.SameSite=Laxsebagai titik awal yang aman.- CORS dibatasi hanya ke origin frontend resmi.
- CSRF aktif untuk semua request stateful.
- Session diregenerasi setelah login.
- Session disimpan di Redis atau database untuk production.
Jika Anda membutuhkan API yang juga dipakai mobile app, third-party client, atau integrasi machine-to-machine, pertimbangkan memisahkan jalur autentikasi: session cookie untuk browser SPA dan token untuk klien non-browser. Dengan begitu, setiap jenis klien memakai mekanisme yang paling sesuai.
Penutup
Laravel cookie session aman untuk SPA dan subdomain bukan sekadar soal membuat login berhasil. Keamanan dan kestabilannya bergantung pada kombinasi konfigurasi cookie, CSRF, CORS, session storage, dan disiplin invalidasi session di server.
Jika Anda menerapkan HTTPS, HttpOnly, Secure, SameSite yang tepat, domain cookie yang benar, regenerasi session setelah login, serta pengujian manual di browser, pendekatan ini dapat menjadi fondasi autentikasi yang kuat dan nyaman untuk SPA berbasis Laravel.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!