3

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:

  1. 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.

  2. First protected route works: After logging in, the first protected route I call (e.g., /getbrochure) works fine—the user is authenticated.

  3. 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:

  1. Is there something about ports and cookies on localhost that I'm missing?

  2. Why does the session ID keep changing for every request, even when cookies are set and all configs seem correct?

  3. 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,
})

2 Answers 2

0

Can you try these configs?

// .env
SESSION_DOMAIN=null

// cors.php
'allowed_origins' => [],

and remove all of the CorsMiddleware.php. This is not needed.

Explanation:

For localhost, I put it as null since localhost isn't really a domain. For production use, it should really be .example.com. So if your site is api.mysystem.com, then you'll need to set it to SESSION_DOMAIN=.mysystem.com.

For the cors.php and CorsMiddleware.php, it depends on what server you are using. Laravel's default cors.php configuration will already set the response headers for you so a custom CorsMiddleware.php is not needed, but don't forget about the actual web server (apache, nginx)

I use nginx and here's my configuration which will set the response headers. If you also set the allowed_origins in cors.php, the browser will show an error that says "duplicate Access-Control-Allow-Origin found", or something along that line.

map $http_origin $origin_allowed {
    default 'OK';
}

map $origin_allowed $origin {
    default null;
    'OK' $http_origin;
}

map $http_user_agent $loggable {
    ~ELB-HealthChecker  0;
    default             1;
}

server {
    listen 80;
    set $origin_allowed_method $origin_allowed$request_method;

    access_log  /dev/stdout main if=$loggable;
    error_log   /dev/stderr warn;

    gzip on;
    gzip_types text/css application/javascript application/json application/font-woff application/font-tff;
    gzip_proxied any;

    charset UTF-8;
    client_max_body_size 256M;
    fastcgi_read_timeout 900;
    # TODO: find out why I need to add this for clinic API endpoints.
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
    root  /var/www/public;
    index index.php index.html index.htm;

    add_header Strict-Transport-Security 'max-age=31536000' always;
    add_header Content-Security-Policy upgrade-insecure-requests;
    add_header X-Content-Type-Options nosniff;
    add_header Referrer-Policy same-origin;


    location / {
        if ($origin_allowed_method = 'OKOPTIONS') {
            add_header Access-Control-Allow-Origin $http_origin always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE';
            add_header Access-Control-Allow-Headers '*, Origin, Authorization, Accept, X-XSRF-TOKEN, Content-Type';
            add_header Access-Control-Max-Age 3600;
            add_header Content-Type 'text/plain charset=UTF-8';
            add_header Content-Length 0;
            return 204;
         }
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~* \.(jpg|jpeg|gif|png|css|js|swf|ico|pdf|svg|eot|ttf|woff)$ {
        if ( $origin_allowed = 'OK') {
            add_header Access-Control-Allow-Origin $http_origin always;
            add_header Access-Control-Allow-Headers 'Origin, Authorization, Accept, Content-Type';
            add_header Access-Control-Allow-Methods 'POST, GET, OPTIONS';
        }

        expires 30d;
        access_log off;
    }

    location ~ \.php$ {
        root /var/www/public;
        # if ( $origin_allowed = 'OK') {
            add_header Access-Control-Allow-Origin $origin always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Access-Control-Allow-Headers '*, Origin, Authorization, Accept, X-XSRF-TOKEN, Content-Type';
            # add_header X-Frame-Options DENY;
            # add_header Cache-control no-store;
            # add_header Pragma no-cache;
        # }
        add_header X-Frame-Options DENY;
        add_header Cache-control no-store;
        add_header Pragma no-cache;

        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass  php-fpm:9000;
        fastcgi_index index.php;

        include       fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO       $fastcgi_path_info;

        fastcgi_connect_timeout 600;
        fastcgi_read_timeout    600;
        fastcgi_send_timeout    600;
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

i've tried SESSION_DOMAIN = NULL multiple times before and it changes nothing. if i use 'allowed_origins' => [], it will not send the csrf-cookie it returns strict-origin-when-cross-origin
May I know what web server you are using and its configuration file?
i am using apache. i fixed the issue. thanks for your help and answering my question ;)
Glad you fixed it. Could you elaborate what was causing the issue? :)
0

Finally, I fixed this problem. I just needed to add http:// in SANCTUM_STATEFUL_DOMAINS, and now the session doesn't change on every request. Here's my working .env:

SESSION_DRIVER=database
SESSION_DOMAIN=null
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
SANCTUM_STATEFUL_DOMAINS=http://localhost:3000

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.