Laravel/Vue SPAs: How to send AJAX requests and not run into CSRF token mismatch exceptions

Serhii Matrunchyk

Creating SPAs or PWAs is very easy in VueJS. As well as creating APIs on Laravel (or Lumen). That’s why I use this pair of Vue + Laravel.

The Problem

And everything is cool until the session is expired and CSRF token is expired too. What to do in this situation? We can’t send API requests anymore because the token is expired. So we can do it either by refreshing a page automatically or asking a user to do so in some messages like “Hey, your session is expired, please click <here> to extend”. Both options are not options actually because we know that the user experience is a pretty important thing so it would be great to not to bother users with randomly refreshing pages (of course there could be some unsaved data) or showing boring messages. The other non-option could be turning off CSRF at all. Which of course is a bad practice and may lead to security pitfalls.

After hours of googling, I found lots of questions and some answers on them but no real solutions which could work for me. Here is my solution to the problem and I hope you will find it useful for your SPA project as well.

The TL;DR; Solution

We can’t just refresh the current page (the page with expired token), but we can make an additional request to the server to retrieve a page with a new token.

Then we need to parse it and retrieve the token.

And the final step is updating the app state with the new token.

Short story long

For sending AJAX requests I use axios. I love it for its simplicity and it just works. I’d recommend you to create a single instance of axios and reuse it everywhere. Also avoid using global helpers like axios.get, axios.post etc to isolate your requests/responses from other packages. For example, there is a known issue with Laravel Echo which causes all requests (and a whole app) to be down in case of broken connection with socket.io server (to be more specific, it causes “Uncaught TypeError: Cannot read property ‘socketId’ of undefined” error which brokes the whole request). Anyhow, using a single instance is pretty easy:

// Http.js
import
axios from 'axios';
// Automatically add CSRF token to every outgoing request
const baseURL = window.App.base_url;
const headers = {
'X-CSRF-TOKEN' : window.Laravel.csrfToken,
'X-Requested-With': 'XMLHttpRequest',
};

const Parent = axios.create({
baseURL,
headers,
});
// Let’s omit this so far, see the explanation below
class Http {} extends Parent;
export default Http;

Very simple! Thus in your files you can just import it an use as usual.

// HttpTest.js
import Http from './Http';
Http.post('/', { abc: 1 })
.then(({ data }) => console.log(data))
.catch(error => console.log(error));

You can set up your interceptors and automatically attach the CSRF token to every outgoing request and track incoming responses by checking whether it’s OK or has some errors:

// Response interceptor
Parent.interceptors.response.use(response => response, error => httpFail(error));

The code above just adds two arrow functions to a response handlers array. So every successful response will be passed through the first arrow function (we just returning what we’ve received without altering it). The main reason for it is to catch and handle failed responses so that we pass it to the httpFail function:

function httpFail(error) {
// Reject on Laravel-driven validation errors
if (error.response && error.response.status === 422) {
return Promise.reject(error);
}

// Refresh tokens and reject to be further handled be the request initiator
if (error.response && error.response.status === 419) {
return refreshAppTokens().then(() => Promise.reject(error));
}

// If internal error
if (error.message && !error.response) {
// Due to a possible bug in Laravel Echo, whitelist Echo server error
// See explanation above
if (error.message === "Cannot read property 'socketId' of undefined") {
// showError(error.message);
return Promise.resolve(error);
}

// Display any other errors to the user and reject
showError(error.message);
return Promise.reject(error);
}

// Redirect to log in page if unauthenticated
if (error.response && error.response.status === 401) {
const router = window.router;
const segments = router.currentRoute.path.split('/');
const isAuth = segments.length > 1 && segments[1] === 'auth';

// If not on main page and not on /auth page (change this block or remove accordingly to your app logic)
if (router.currentRoute.path !== '/' && !isAuth) {
store.dispatch('resetAuthorizedUser');
window.router.push(`/auth/login?back=${router.currentRoute.path}`);
}
return Promise.reject(error);
}

// Redirect if the backend asks it
if (error.response && error.response.status === 402 && error.response.data.redirect) {
window.router.push(error.response.data.redirect);
return Promise.reject(error);
}

// Show all other errors
showHttpError(error);
return Promise.reject(error);
}

If you’re still with me after this chunk of the code, I want to give you a free Web Animations effect as a gift! :)

I hope the code is self-explanatory except refreshAppTokens() function unmentioned before. This is the place where we fetch a new CSRF token:

function refreshAppTokens() {
// Retrieve a new page with a fresh token
axios.get('/')
.then(({ data }) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = data;
return div.querySelector('meta[name=csrf-token]').getAttribute('content');
})
.then((token) => {
axios.defaults.headers['X-CSRF-TOKEN'] = token;
window.Laravel.csrfToken = token;
document.querySelector('meta[name=csrf-token]').setAttribute('content', token);
});
}

Finally, the last piece of the puzzle is code which sends the request and after it catches 419 error, it tries to re-send the failed request it once again (with a new token, see refreshAppTokens above). Since we want to handle every POST request, let’s modify our Http class as follows:

class Http extends Parent {
static post(url, data, config) {
return new Promise((resolve, reject) => {
Parent.post(url, data, config)
.then(response1 => resolve(response1))
.catch((error1) => {
console.warn('CSRF token is expired'); // eslint-disable-line no-console
// There is one more try for token mismatch error
if (error1.response && error1.response.status === 419) {
Parent.post(url, data, config)
.then((response2) => {
const u = new window.URL(decodeURIComponent(location.href));
location.href = `${location.origin}${u.searchParams.get('back')}`;
resolve(response2);
})
.catch(error2 => reject(error2));
}
});
});
}
}

It just overrides a post static method of axios and hijacks it with some changes. I hope it self-explanatory as well. I believe it might be written in more clear way (there’s always a space for refactoring), but, you got the idea.

Afterwords

With a minimum changes and avoiding to touch Laravel core we’ve achieved the desired result: 1) we kept the CSRF token and didn’t decrease a security layer; 2) we retrieved the new CSRF token without reloading the page and transparently for the user, which is cool in terms of UX. So when the token is expired, the user would not even notice that something just happened and his/her data will be saved!

We just made the Internet a bit more friendly than it was before!

Serhii Matrunchyk

Written by

Owner & CEO @Digital Idea. Professional Full-Stack Web Developer. Lives in: Lutsk, Ukraine. Frameworks: VueJs, Node, Laravel, WordPress, AWS.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade