Here's the situation I'm facing in my project:
Stack:
- Backend: Laravel 12 (session driver: database, Sanctum SPA authentication)
- Frontend: Nuxt 3
- Backend runs at: localhost:8000
- Frontend runs at: localhost:3000
Issue Description:
Login works: When I log in from the frontend, everything seems fine. Laravel logs show a session is created, and the login API returns the correct user information.
First protected route works: After logging in, the first protected route I call (e.g., /getbrochure) works fine—the user is authenticated.
Second protected route fails (CSRF Token Mismatch): But as soon as I call a second protected route (e.g., /logout or /uploadbrochure), Laravel returns a CSRF Token Mismatch error. This happens no matter the order of the protected requests—for example, login -> update_user -> get_user will trigger the CSRF error on the second request.
Debug Info: Laravel session ID keeps changing: Checking my Laravel logs, I noticed that the session_id changes on every single request, meaning every request starts a brand new session. Because of that, the CSRF token in the session never matches the one in the request, resulting in a CSRF error immediately after the first protected route.
I intentionally use SESSION_DRIVER=database because I want my session to be stored securely on the backend and remain consistent between requests (unlike cookie driver, which only stores the session in the browser). With session driver: database, the session ID from the browser must remain the same for all authenticated requests so Laravel can retrieve the correct session data. But in my current setup, every request creates a new session, so authentication and CSRF verification always fail after the first request.
The problem I can't figure out:
Is there something about ports and cookies on localhost that I'm missing?
Why does the session ID keep changing for every request, even when cookies are set and all configs seem correct?
Is it possible to get Laravel session-based authentication working between frontend and backend when they're running on different ports (e.g., localhost:3000 and localhost:8000)?
Any insight into this would be really helpful, because I feel like I've tried all the right configurations, but the session/cookie just doesn't persist across requests.
My config:
.env
SESSION_DOMAIN=.localhost
SANCTUM_STATEFUL_DOMAINS=localhost,localhost:3000,127.0.0.1,127.0.0.1:3000
SESSION_DRIVER=database
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
config/cors
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'oauth/*'],
'supports_credentials' => true,
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:3000'],
// ...
];
config/session
return [
'driver' => env('SESSION_DRIVER', 'cookie'),
'lifetime' => (int) env('SESSION_LIFETIME', 30),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', true),
'encrypt' => env('SESSION_ENCRYPT', true),
'cookie' => env('SESSION_COOKIE', Str::slug(env('APP_NAME', 'laravel'), '_').'_session'),
'path' => env('SESSION_PATH', '/'),
'domain' => env('SESSION_DOMAIN', null),
'secure' => env('SESSION_SECURE_COOKIE', false),
'http_only' => env('SESSION_HTTP_ONLY', true),
'same_site' => env('SESSION_SAME_SITE', 'lax'),
//...
];
config/sanctum
return[
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')),
];
corsMiddleware
class CorsMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Access-Control-Allow-Origin', 'http://localhost:3000');
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, X-Token-Auth, Authorization, X-XSRF-TOKEN');
$response->headers->set('Access-Control-Allow-Credentials', 'true');
$response->headers->set('Access-Control-Expose-Headers', 'XSRF-TOKEN, Authorization');
return $response;
}
}
Route api.php
Route::prefix('v1')->middleware('api')->group(function () {
Route::middleware(['web', EnsureFrontendRequestsAreStateful::class])->group(function () {
Route::post('/login', [AuthController::class, 'AdminAffiliateLogin']);
Route::post('/logincustomer', [AuthController::class, 'CustomerLogin']);
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/getallbrochure', [BrochureController::class, 'index']);
Route::get('/getbrochure/{id}', [BrochureController::class, 'show']);
Route::post('/uploadbrochure', [BrochureController::class, 'store']);
Route::get('/deletebrochure/{id}', [BrochureController::class, 'destroy']);
Route::put('/updatebrochure/{id}', [BrochureController::class, 'update']);
});
});
Route::post('/register', [AuthController::class, 'CustomerRegister']);
});
Axios.ts
const instance: AxiosInstance = axios.create({ baseURL: apiBaseUrl, withCredentials: true })
const csrfAxios: AxiosInstance = axios.create({
baseURL: backendUrl,
withCredentials: true,
})
const sessionAxios: AxiosInstance = axios.create({
baseURL: apiBaseUrl,
withCredentials: true,
})