Kasus yang sering membingungkan saat memakai Laravel Sanctum adalah: proses login berhasil, cookie terlihat terset, tetapi setiap request API dari subdomain tetap mendapat 401 Unauthorized. Gejala ini umum terjadi pada arsitektur seperti frontend di app.example.com dan API di api.example.com, terutama saat pindah dari lokal ke staging atau production.
Masalahnya biasanya bukan pada endpoint login itu sendiri, melainkan pada cara browser mengirim cookie sesi lintas subdomain, bagaimana Laravel menandai request sebagai stateful, dan apakah konfigurasi CORS, domain cookie, SameSite, serta HTTPS sudah selaras. Di artikel ini, fokusnya adalah membedah konfigurasi yang paling sering menjadi penyebab: SANCTUM_STATEFUL_DOMAINS, SESSION_DOMAIN, CORS, SameSite, dan cookie secure.
Memahami akar masalah: login sukses, API tetap 401
Pada mode SPA, Sanctum tidak bekerja seperti token bearer biasa. Sanctum mengandalkan session cookie milik Laravel. Alur sederhananya seperti ini:
- Frontend memanggil endpoint CSRF cookie, biasanya
/sanctum/csrf-cookie. - Browser menerima cookie seperti
XSRF-TOKENdan cookie sesi. - Frontend melakukan login ke endpoint login.
- Setelah login sukses, browser harus terus mengirim cookie sesi saat memanggil endpoint API yang dilindungi.
Kalau langkah 4 gagal, API akan menganggap user belum terautentikasi dan mengembalikan 401. Penyebab gagalnya langkah ini biasanya salah satu dari berikut:
- Subdomain frontend tidak masuk ke
SANCTUM_STATEFUL_DOMAINS. SESSION_DOMAINsalah, sehingga cookie tidak berlaku untuk subdomain yang dibutuhkan.- Request lintas origin tidak mengirim kredensial karena konfigurasi CORS atau fetch/axios salah.
- Cookie ditolak browser karena atribut
SameSiteatauSecuretidak cocok dengan skenario HTTPS. - Environment staging/production berada di belakang proxy sehingga Laravel salah mendeteksi skema HTTP/HTTPS.
Apa arti stateful pada Sanctum?
Sanctum membedakan request stateful dan stateless. Untuk SPA yang berjalan di domain milik kita sendiri, Sanctum mengharapkan request dianggap stateful, artinya autentikasi dilakukan lewat session cookie. Daftar origin/domain yang boleh diperlakukan seperti ini ditentukan oleh SANCTUM_STATEFUL_DOMAINS.
Jika frontend Anda berjalan di app.example.com tetapi nilai SANCTUM_STATEFUL_DOMAINS hanya berisi localhost, maka request dari subdomain itu bisa dianggap bukan request stateful. Akibatnya, meskipun cookie ada, middleware Sanctum tidak memprosesnya seperti yang diharapkan.
Contoh konfigurasi .env
SANCTUM_STATEFUL_DOMAINS=app.example.com,admin.example.com,staging-app.example.com
SESSION_DOMAIN=.example.com
SESSION_DRIVER=cookie
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
APP_URL=https://api.example.comBeberapa catatan penting:
SANCTUM_STATEFUL_DOMAINSberisi host frontend yang mengakses API, bukan host API itu sendiri semata.SESSION_DOMAIN=.example.comdengan titik di depan membuat cookie dapat dipakai lintas subdomain sepertiapp.example.comdanapi.example.com.- Untuk staging yang memakai domain berbeda, misalnya
staging.example.net, Anda harus menyesuaikanSESSION_DOMAINdanSANCTUM_STATEFUL_DOMAINSuntuk domain tersebut. Jangan mengasumsikan satu konfigurasi cocok untuk semua lingkungan.
Kesalahan umum pada SANCTUM_STATEFUL_DOMAINS
- Memasukkan URL lengkap dengan skema, misalnya
https://app.example.com. Umumnya yang dibutuhkan adalah host dan bila perlu port, bukan URL penuh. - Lupa menambahkan port saat development, misalnya
localhost:3000. - Hanya memasukkan domain API, padahal frontend berjalan di host lain.
- Sudah mengubah
.env, tetapi belum menjalankanphp artisan config:clearatauphp artisan optimize:clear.
SESSION_DOMAIN: penentu apakah cookie bisa dibaca lintas subdomain
Jika login berhasil tetapi cookie sesi hanya berlaku untuk api.example.com, browser tidak akan mengirimkannya saat konteks request berasal dari subdomain lain dengan aturan cookie tertentu. Karena itu, untuk skenario subdomain dalam satu root domain yang sama, konfigurasi paling umum adalah:
SESSION_DOMAIN=.example.comDengan ini, cookie sesi dapat digunakan oleh seluruh subdomain di bawah example.com. Jika Anda menulis SESSION_DOMAIN=api.example.com, cookie menjadi terlalu sempit cakupannya.
Jika frontend dan backend berada pada root domain yang benar-benar berbeda, misalnya
frontend.example.comdanapi.example.org, masalahnya bukan lagi sekadar subdomain. Aturan cookie browser akan lebih ketat, dan pendekatannya harus dievaluasi ulang.
Verifikasi domain cookie di DevTools
Buka browser DevTools, lalu cek tab Application atau Storage, bagian Cookies. Perhatikan:
- Apakah cookie sesi benar-benar ada?
- Nilai Domain-nya apakah
.example.comatau terlalu spesifik? - Path biasanya
/. - Apakah cookie bertanda Secure?
- Apakah atribut SameSite sesuai dengan skenario Anda?
CORS dan credentials: cookie tidak akan terkirim jika ini salah
Banyak kasus 401 ternyata bukan karena Sanctum, melainkan browser tidak pernah mengirim cookie ke API. Untuk request lintas origin, Anda harus mengizinkan credentials di dua sisi:
- Frontend harus mengirim request dengan
withCredentialsataucredentials: 'include'. - Backend harus mengizinkan kredensial lewat konfigurasi CORS.
Contoh frontend dengan Axios
import axios from 'axios';
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;
await axios.get('https://api.example.com/sanctum/csrf-cookie');
await axios.post('https://api.example.com/login', {
email: 'user@example.com',
password: 'secret'
});
const response = await axios.get('https://api.example.com/api/user');Contoh konfigurasi CORS Laravel
return [
'paths' => ['api/*', 'login', 'logout', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://app.example.com'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];Hal penting di sini:
supports_credentialsharustrue.- Jika memakai credentials, jangan gunakan
*untukallowed_origins. Browser akan menolak kombinasi itu. - Pastikan path seperti
sanctum/csrf-cookie,login, dan endpoint API Anda memang termasuk dalam CORS config.
SameSite dan Secure cookie di staging/production
Pada lingkungan modern, browser sangat ketat terhadap cookie. Dua atribut yang sering berpengaruh adalah SameSite dan Secure.
SameSite
Untuk skenario frontend dan API yang masih berada di satu situs utama dengan subdomain berbeda, SESSION_SAME_SITE=lax sering cukup. Namun, perilaku browser bergantung pada konteks request dan cara aplikasi dijalankan. Jika arsitektur Anda benar-benar dianggap lintas situs oleh browser atau ada integrasi melalui iframe/cross-site request tertentu, SameSite=None mungkin diperlukan.
Tetapi ada aturan penting: jika memakai SameSite=None, cookie harus Secure. Artinya hanya akan dikirim lewat HTTPS.
SESSION_SAME_SITE=lax
SESSION_SECURE_COOKIE=trueAtau jika memang perlu:
SESSION_SAME_SITE=none
SESSION_SECURE_COOKIE=trueSecure cookie
Di staging/production yang sudah HTTPS, sangat disarankan memakai:
SESSION_SECURE_COOKIE=trueKalau nilainya true tetapi aplikasi sebenarnya diakses melalui HTTP, cookie tidak akan terkirim. Sebaliknya, jika production sudah HTTPS tetapi cookie tidak ditandai secure, Anda bisa menemui perilaku yang tidak konsisten dan risiko keamanan lebih besar.
Waspadai reverse proxy atau load balancer
Bila Laravel berada di belakang Nginx proxy, ingress Kubernetes, Cloudflare, atau load balancer, Laravel harus bisa mendeteksi bahwa request asli datang melalui HTTPS. Jika tidak, aplikasi mungkin menghasilkan cookie atau redirect yang tidak konsisten.
Pastikan header proxy diteruskan dengan benar dan konfigurasi trusted proxies sesuai. Gejala klasiknya: browser menerima cookie, tetapi atributnya tidak seperti yang diharapkan, atau redirect bolak-balik antara HTTP dan HTTPS.
Contoh konfigurasi yang umum benar untuk subdomain
Misalkan arsitekturnya seperti ini:
- Frontend SPA:
https://app.example.com - API Laravel:
https://api.example.com
.env pada API:
APP_URL=https://api.example.com
FRONTEND_URL=https://app.example.com
SANCTUM_STATEFUL_DOMAINS=app.example.com
SESSION_DOMAIN=.example.com
SESSION_DRIVER=cookie
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=laxconfig/cors.php:
return [
'paths' => ['api/*', 'login', 'logout', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://app.example.com'],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];Frontend harus mengirim credentials:
fetch('https://api.example.com/api/user', {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});Setelah mengubah konfigurasi, jalankan:
php artisan optimize:clearChecklist debug di browser DevTools
Jika masih 401, lakukan pengecekan ini secara sistematis. Jangan menebak-nebak; lihat apa yang benar-benar dikirim browser.
1. Cek request ke /sanctum/csrf-cookie
- Status harus sukses.
- Response header harus berisi
Set-Cookie. - Cookie
XSRF-TOKENdan sesi harus tersimpan.
2. Cek request login
- Pastikan request membawa kredensial.
- Pastikan response login juga menyetel atau memperbarui cookie sesi.
- Periksa apakah ada warning seperti cookie blocked, This Set-Cookie was blocked..., atau masalah
SameSite/Secure.
3. Cek request ke endpoint API yang 401
- Apakah request header memiliki
Cookie? - Apakah origin sesuai dengan yang diizinkan CORS?
- Apakah response CORS menyertakan
Access-Control-Allow-Credentials: true?
4. Cek tab Cookies di DevTools
- Domain cookie benar?
- Secure aktif saat HTTPS?
- SameSite cocok?
- Cookie expired atau terhapus?
5. Cek konfigurasi server
- HTTPS benar-benar aktif end-to-end?
- Proxy meneruskan header HTTPS dengan benar?
- Tidak ada redirect aneh antar-subdomain?
Kesalahan yang paling sering terjadi
SANCTUM_STATEFUL_DOMAINSbelum memasukkan domain frontend staging/production.SESSION_DOMAINterlalu sempit, misalnya hanyaapi.example.com.- CORS masih memakai wildcard padahal request mengirim credentials.
- Frontend lupa
withCredentialsataucredentials: 'include'. SESSION_SECURE_COOKIE=truedipakai pada lingkungan yang belum HTTPS penuh.SameSite=Nonedipakai tanpaSecure, sehingga cookie diblokir browser.- Perubahan env/config belum diterapkan karena cache config masih aktif.
Penutup
Jika login Sanctum berhasil tetapi request API dari subdomain tetap 401, hampir selalu akar masalahnya ada pada cookie yang tidak ikut terkirim atau tidak dikenali sebagai stateful request. Karena itu, fokus debugging sebaiknya dimulai dari lima titik: SANCTUM_STATEFUL_DOMAINS, SESSION_DOMAIN, CORS credentials, SameSite, dan Secure cookie.
Pendekatan terbaik bukan sekadar mengubah config secara acak, tetapi memverifikasi alur di browser DevTools: apakah cookie disetel, apakah browser menyimpannya, dan apakah cookie benar-benar terkirim pada request yang gagal. Begitu alur ini terlihat jelas, penyebab 401 biasanya cepat ditemukan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!