dmytroshved's avatar

Laravel Sanctum SPA Auth: 419 Token mismatch error in logout

Hey everyone

I am struggling with annoying 419 error trying to logout. The login and register are working fine. My api and spa are on the same top-level domain, but different ports

api - localhost:8000 spa (vue) - localhost:5173

After hours of debugging and changing the different settings I still get the 419

backend:

env

APP_URL=http://localhost:8000
FRONTEND_URL=http://localhost:5173
SANCTUM_STATEFUL_DOMAINS=localhost:5173
SESSION_DOMAIN=localhost

bootstrap/app.php

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
        apiPrefix: 'api/v1',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->alias([
            'role' => RoleMiddleware::class,
        ]);

        $middleware->api(prepend: [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        ]);

        $middleware->statefulApi();
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

cors

'paths' => ['*'],

    'allowed_methods' => ['*'],

    'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

sanctum

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,localhost:5173,127.0.0.1,127.0.0.1:8000,::1',
        Sanctum::currentApplicationUrlWithPort(),
        // Sanctum::currentRequestHost(),
    ))),

authentication routes in routes/web.php

Route::post('login', LoginController::class);
Route::post('register', RegisterController::class);
Route::post('logout', LogoutController::class);

LogoutController.php

public function __invoke(Request $request)
    {
        Auth::logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return response()->json([
           'message' => 'Logged out'
        ]);
    }

frontend:

axios.js

import axios from 'axios'
import router from '@/router/index.js'

const axiosClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  withCredentials: true,
  withXSRFToken: true,
})

axiosClient.interceptors.response.use( (response) =>{
  return response;
}, error => {
  if (error.response && error.response.status === 401){
    router.push({name: 'Login'})
  }

  throw error;
})

export default axiosClient

env

VITE_API_BASE_URL=http://localhost:8000

logout function

function logout() {
  axiosClient.post('/logout')
    .then((response) => {
      router.push({name: 'Login'})
    })
}

for reference

login function

const data = ref({
  email: '',
  password: '',
})

const errorMessage = ref('')

function submit() {
  axiosClient.get('/sanctum/csrf-cookie').then(response => {
    axiosClient.post('/login', data.value)
      .then(response => {
        router.push({name: 'Home'})
      })
      .catch(error => {
        console.log(error.response)
        errorMessage.value = error.response.data.message;
      })
  });
}

How I tried to debug logout

  • login
  • check cookies are set
  • use logout (error shows)

Would be grateful for your help

Best regards

0 likes
13 replies
JussiMannisto's avatar

When is the 419 thrown? Is it on the logout request itself, or some other action after that? Check the network tab in the browser's dev tools.

Are you regenerating the CSRF token at the end of a login / register, like you're doing in logout?

Regardless, Laravel should include a fresh XSRF-TOKEN cookie in the login or register response, and Axios should pick that up. If that's not working, further debugging is needed.

2 likes
dmytroshved's avatar

The error happens in the request to /logout, I mean when I try to logout, I hit the button, it emits the logout function and then I see 419 in the browser, however preflight request was successfull

Are you regenerating the CSRF token at the end of a login / register, like you're doing in logout?

Im not sure, I am regenerating the session in the login, but at the register I don't

RegisterController

public function __invoke(RegisterRequest $request)
    {
        $user = User::create($request->validated());

        Auth::login($user);

        return (new UserResource($user));
    }

LoginController

public function __invoke(LoginRequest $request)
    {
        if (Auth::attempt($request->validated())){
            $request->session()->regenerate();

            return (new UserResource(Auth::user()));
        }else{
            return response()->json([
                'message' => 'The provided credentials are incorrect'
            ], 422);
        }
    }
1 like
dmytroshved's avatar

Some good guy helped me with that, so I am leaving the answer here

You would still need to call the /sanctum/csrf-cookie as your /logout endpoint is a POST request.

All of laravel's POST,PUT, PATCH, DELETE requests require you to have the csrf cookie as mentioned here in the docs.


Fixed logout function

function logout() {
  axiosClient.get('/sanctum/csrf-cookie').then(response => {
    axiosClient.post('/logout')
  });
}
2 likes
JussiMannisto's avatar

POST endpoints require a CSRF token, of course. The token is included in the XSRF-TOKEN cookie you receive in responses, and Axios automatically includes it as a request header.

The real question is why the token stops working after register or login. You shouldn't need to call the /sanctum/csrf-cookie endpoint before every POST request. Once should be enough. Even if the token was rotated during login (which it is not), the new token cookie would be included in the login response.

There must be something wrong with your setup.

Edit. Are you doing anything unusual related to origins, like redirecting to a different subdomain or port after logging in, and then attempting to log out from there?

2 likes
dmytroshved's avatar

I'm not sure I get it right, so should I remove /sanctum/csrf-token call in the logout?

Are you doing anything unusual related to origins, like redirecting to a different subdomain or port after logging in, and then attempting to log out from there?

No, after login I am redirecting to the Home page, nothing fancy here

function submit() {
  axiosClient.get('/sanctum/csrf-cookie').then(response => {
    axiosClient.post('/login', data.value)
      .then(response => {
        router.push({name: 'Home'})
      })
      .catch(error => {
        console.log(error.response)
        errorMessage.value = error.response.data.message;
      })
  });
}

EDIT

Am I making a mistake placing my auth routes in routes/web.php instead of routes/api.php?

Route::post('login', LoginController::class);
Route::post('register', RegisterController::class);
Route::post('logout', LogoutController::class);
1 like
JussiMannisto's avatar

I'm not sure I get it right, so should I remove /sanctum/csrf-token call in the logout?

Yes. It shouldn't be needed, because the front end should already have the token: you only need to call the endpoint once at the start of the session. If it doesn't work, something's wrong.

Am I making a mistake placing my auth routes in routes/web.php instead of routes/api.php?

Yes. When you're using Laravel as a stateful API, those routes should be under api.php. Also, make sure to apply the auth:sanctum middleware to protected routes, e.g. the logout route.

https://laravel.com/docs/12.x/sanctum#protecting-mobile-api-routes

2 likes
dmytroshved's avatar

Okay I have an interesting question

Should I configure my SESSION_DRIVER to cookie instead of default DATABASE?

Because when I changed this thing my spa is not throwing 419 and everything working well!

Example

+ SESSION_DRIVER=cookie
- SESSION_DRIVER=database

EDIT

I also got a new idea how to fix it while keeping my SESSION_DRIVER=database

In my project it was necessary to use UUID for users (I know about their disadvantage, but it wasn't mine idea)

what if in my session table there was no user_id and thus I got an 419 because application didn't see users id in auth session?

JakeMiller's avatar

The 419 “Token mismatch” error usually happens when the CSRF token isn’t sent correctly from your SPA to the Laravel backend. Make sure your frontend includes withCredentials: true in your Axios or fetch requests, and that your SANCTUM_STATEFUL_DOMAINS in .env includes the SPA domain (localhost:5173). Also, check that the session cookie domain is correctly set so the token is properly shared between ports.

1 like
shaneomac's avatar

You should not need this " axiosClient.get('/sanctum/csrf-cookie').then(response => { " What does your app.blade.php look like?

You don't have the CSRF loaded in the head do you?

JussiMannisto's avatar

It's needed because they're using Laravel as an API backend for a SPA. The app.blade.php file isn't used.

dmytroshved's avatar
dmytroshved
OP
Best Answer
Level 6

I finally noticed where I had a mistake

My session table didn't have an user_id because I was using UUID for my users, and session table wasn't properly configured for such UUID

Fixed session migration (notice that session table included in the create_users_table migration in laravel 12)

Schema::create('sessions', function (Blueprint $table) {
    $table->string('id')->primary();
    $table->foreignUuid('user_id')->nullable()->index(); // fixed line to use UUID
    $table->string('ip_address', 45)->nullable();
    $table->text('user_agent')->nullable();
    $table->longText('payload');
    $table->integer('last_activity')->index();
});

EDIT

I also wanna to leave the important configuration places you need to check if you're getting errors

Config settings to check:

backend

1. Check your SPA (e.g. VueJS app) and API (e.g. Laravel) are on the same top level domain

For example:

api - localhost:8000

spa - localhost:5173

(notice: localhost = 127.0.0.1:8000 is a top-level domain)

2. Ensure to properly configure .env

APP_URL=http://localhost:8000
FRONTEND_URL=http://localhost:5173
SANCTUM_STATEFUL_DOMAINS=localhost:5173
SESSION_DOMAIN=localhost

3. In your bootstrap/app.php in the withMiddleware

$middleware->statefulApi(); 

4. Ensure config/cors.php is properly configured

Notice: If you don't have this file run: php artisan config:publish cors

    'paths' => ['*'], // optionally config this line (see usual config below)

    'allowed_methods' => ['*'], // EXAMINE THIS LINE 

    'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], // v

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],
    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true, // AND EXAMINE THIS LINE (throws CORS error if false)

usually you can see this kind of config for paths in config/cors.php

'paths' => ['api/*', 'sanctum/csrf-cookie', '/login', '/logout'],

5. Session and SESSION_DRIVER

In your .env file check SESSION_DRIVER=database (it's a default value and you don't need to touch it)

Check user_id is set after registration/login in your sessions table

Reminder:

  • You don't need to call the /sanctum/csrf-cookie in logout, because after yout logged in or registered the frontend should already have the token

  • Authentication routes (login, register, logout) should be in routes/api.php not routes/web.php

frontend

1. Check your axiosClient (if you're using one, or chec your axios are using proper settings)

Example of my src/axios.js

import axios from 'axios'
import router from '@/router/index.js'

const axiosClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // check how to configure this line below
  withCredentials: true, // THIS LINE IS IMPORTANT
  withXSRFToken: true, // THIS LINE IS ALSO IMPORTANT
})

axiosClient.interceptors.response.use( (response) =>{
  return response;
}, error => {
  if (error.response && error.response.status === 401){
    router.push({name: 'Login'})
  }

  throw error;
})

export default axiosClient

in your .env (notice: its a SPA's .env file, not an API's) check you have your API base URL, for example

VITE_API_BASE_URL=http://localhost:8000

2. Check your routes are sending the GET /sanctum/csrf-cookie request before POST, PUT, PATCH, DELETE

example: login request

const form = reactive({
  email: '',
  password: '',
})

const errorMessage = ref('')

function submit() {
  // THIS LINE IS IMPORTANT
  axiosClient.get('/sanctum/csrf-cookie').then(response => {
    axiosClient.post('/login', form)
      .then(response => {
        router.push({name: 'Home'})
      })
      .catch(error => {
        errorMessage.value = error.response.data.message;
      })
  });
}

Reminder:

  • You shouldn't call GET /sanctum/csrf-cookie before EVERY POST, PUT, PATCH, DELETE requests

Remember this: You only need to call the endpoint once at the start of the session.

Hope it'll provide clear understanding where you can find your error

Really appreciate the support I got from:

@jussimannisto

Please or to participate in this conversation.