vue.jssingle-page-applicationlaravel-sanctumlaravel-apilaravel-fortify

Error 419 (unknown status) CSRF token mismatch. | Laravel Sanctum + Fortify + Vue SPA


I get a "419 (unknown status)" error when sending a post request to login user

POST http://localhost/login 419 (unknown status) Network request response : "message": "CSRF token mismatch." Laravel server : http://localhost:80 (XAMPP default port)

Vue server : http://localhost:5173

LoginComponent.vue

<script setup>
import { useRouter } from 'vue-router'
import { useAuthStore } from '../../stores/Auth'
const authUserStore = useAuthStore()
const router = useRouter()

import AuthService from '@/services/AuthService'
import { ref } from 'vue'

const email = ref('')
const password = ref('')
const error = ref('')

const login = async () => {
  const payload = {
    email: email.value,
    password: password.value
  }
  error.value = null
  try {
    await AuthService.login(payload)
    const authUser = await authUserStore.getAuthUser()
    if (authUser) {
      authUserStore.setGuest({ value: 'isNotGuest' })
      router.push('/')
    } else {
      const error = Error('Unable to fetch user after login, check your API settings.')
      error.name = 'Fetch User'
      throw error
    }
  } catch (error) {
    error.value = getError(error)
  }
}
</script>

<template>
      <form @submit.prevent="login">
        <div>
          <label>Email address</label>
          <input v-model="email" />
        </div>

        <div>
          <label>Password</label>
          <inpu v-model="password" />
        </div>

        <button type="submit">Sign in</button>
      </form>
</template>

services/AuthService.js

export const authClient = axios.create({
  baseURL: 'http://localhost:80',
  withCredentials: true // required to handle the CSRF token
})

/*
 * Add a response interceptor
 */
authClient.interceptors.response.use(
  (response) => {
    return response
  },
  function (error) {
    const authUserStore = useAuthStore()
    if (error.response && (error.response.status === 401 || error.response.status === 419)) {
      authUserStore.logout()
    }
    return Promise.reject(error)
  }
)

export default {
  async login(payload) {
    await authClient.get('/sanctum/csrf-cookie')
    await authClient.post('/login', payload)
  },
}

stores/Auth.js

import { defineStore } from 'pinia'
import AuthService from '@/services/AuthService'
export const useAuthStore = defineStore('auth', {
  state: () => ({ user: null, loading: false, error: null }),
  getters: {
    guest: () => {
      const storageItem = window.localStorage.getItem('guest')
      if (!storageItem) return false
      if (storageItem === 'isGuest') return true
      if (storageItem === 'isNotGuest') return false
    }
  },
  actions: {
    SET_USER(state, user) {
      state.user = user
    },
    async getAuthUser() {
      this.SET_LOADING(true)
      try {
        const response = await AuthService.getAuthUser()
        this.SET_USER(response.data.data)
        return response.data.data
      } catch (error) {
        this.SET_USER(null)
      }
    },
    setGuest({ value }) {
      window.localStorage.setItem('guest', value)
    },

  }
})

env variables

SESSION_DRIVER=cookie
SANCTUM_STATEFUL_DOMAINS=http://localhost:5173
SPA_URL=http://localhost:5173
SESSION_DOMAIN=localhost

cors.php

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

    'allowed_origins' => [env('SPA_URL','http://localhost:5173')],

fortify.php

    'home' => env('SPA_URL') . '/',
    'views' => false,

    'features' => [
        Features::registration(),
    ],

sanctum.php

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

    'middleware' => [
        'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
    ],

bootstrap.js

import axios from "axios";
window.axios = axios;

window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
window.axios.defaults.withCredentials = true;
window.axios.defaults.withXSRFToken = true;

Solution

  • It appeared that axios doesn't send CSRF-TOKEN in the login request headers by deafault, So i had to grap the cookie initiated from the request to sanctum/csrf-cookie, and then send it manually with the request headers to /login route:

    function getCookie(name) {
      const value = document.cookie
      const parts = value.split(name)
      if (parts.length === 2) {
        return parts.pop().split(';').shift()
      }
    }
    
    const login = () => {
      axios.get('http://localhost:80/sanctum/csrf-cookie', {
        withCredentials: true
      })
      const csrfToken = getCookie('XSRF-TOKEN=')
    
      const payload = {
        email: email.value,
        password: password.value
      }
    
      axios.post('http://localhost/login', payload, {
        headers: {
          'X-XSRF-TOKEN': decodeURIComponent(csrfToken)
        },
        withCredentials: true
      })
    }