Skip to content
Advertisement

401 race conditions with axios/laravel – CSRF/Unauthenticated

I have a SPA application that is powered via axios. Now, I originally noticed that the CRSF token was being dropped randomly which we identified as a race condition when two requests fired concurrently. I since changed the storage mechanism for sessions to the database and added atomic blocking to my routes and disabled the requirement for CSRF for routes that where not “action” routes.

Now, what is interesting, I get a 401 unauthenticated instead (randomly). I found a post online about reattempting to capture all the new session/token so I wrote the following:

let retries = [];

const sessionHandler = async error => {
    if (error.response && (error.response.status === 419 || error.response.status === 401)) {
        const uri = error.response.config.url;
        const data = error.response.config.data;

        // I use nothing from this endpoint, its simply to allow
        // axios to get the CSRF token
        const endpoint = '/session/user-config';

        // Push to retries
        retries.push({uri: uri, data: data});

        // After exhausting retries, force refresh
        if (retries.filter(attempt => attempt.uri === uri).length > 3 || retries.filter(attempt => attempt.uri === endpoint).length > 3) {
            window.location.reload();
        }

        // Attempt to obtain new token
        return await axios.get(endpoint).then(async () => {
            // Attempt to fulfill request
            return await axios.post(uri, data).then(response => response);
        });
    }

    return Promise.reject(error);
};

window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

axios.interceptors.response.use(response => {
    // Remove any attempts after a successful request
    retries.filter(attempt => attempt.uri = response.config.url).forEach(attempt => {
        retries.slice(retries.indexOf(attempt), 1);
    });

    return response;
}, sessionHandler);

This successfully (70% of the time) grabs a new token should any 419 errors get thrown. Now, for the other 30%, it loops and eventually reloads the page. During this reload, after a 401 Unauthenticated 3 times, it redirects to login.

This can happen within the first 2 minutes of logging in sometimes. I have increased the session lifetime to 1 year to debug, I have tried everything locally to reproduce this, as well as on dev/test servers and yet it seems to be only effecting the production server.

I’ve tried to check all the configuration for differences but none can be found. I cannot find any known issues surrounding this and the 70% is not a solution but more a duck-tape fix. Does any one know if there is something additional I can do to laravel or axios to prevent this issue?

This happens randomly, I can click around really fast for ages and it won’t happen, then randomly, it will happen.

Update: I have noticed that every request, made by axios, updates the local cookie value of the session, I’m unsure if this is normal behaviour but potentially it could be encountering a read/write issue? Is it even possible to add blocking to cookies?

When watching the request, both the CSRF and session cookies are sent. The request appears fine but yet it results in either a 419 CSRF token issue or a 401 Unauthenticated. I then resend the request and it works fine – it is totally randomly failing.

Below is an example of what can been seen, one request got a 401 response code but yet all the others worked perfectly fine.

401

Sometimes, all of these might 401. Inside the request, the CSRF token and laravel_session is sent in the headers and cookies. No signs of anything missing or dropping out as seen below in the request:

419 Request:

419

401 Request:

401

Another update, after using the debugger tools, I can see Axios runs into an exception during a 401:

Exception Axios

The full exception is detailed below but only triggers during a 401:

TypeError: ‘caller’, ‘callee’, and ‘arguments’ properties may not be accessed on strict mode functions or the arguments objects for calls to them

Advertisement

Answer

From the original post and comments, it seems like you’re using the default Laravel authentication for asynchronous communication with the server. I feel like this is ultimately the root of your issue and you should look into utilizing Passport since you mentioned it was already installed. (If it isn’t already installed, I recommend Sanctum over Passport for what you describe here because it’s simpler.) There are assumptions made that the server will be controlling the application state when you utilize the default authentication.

You also mention that you have to use the default authentication gateway to retrieve the currently logged in user. This means that either you didn’t authenticate against Passport correctly or that you’re not setting the OAuth token correctly in your requests. Assuming that everything is done correctly, you can get the user from a Passport based request the same way you would get from the default guard.

I know this likely isn’t the answer you’re looking for, but based on what you’re saying it sounds like you’ve spent more time fighting against the grain of the default authentication middleware than it would take to implement something more suited to the task.

The Original Problem

The reason that it is intermittent is, as you guessed, a race condition.

When you make a request to Laravel, there is a varying window of time that you can make another request with the same session information that it will be allowed through. This is because the Session information isn’t invalidated and “re-salted” immediately, so the original salted session information is still valid for a short time.

Additionally, since the session info comes from a cookie, if the original request returns before the second request is sent, then the cookie is updated to the new value and sent along as expected and will also work.

Essentially, you’re getting a 401 because you had a message “in-flight” as the cookie value was changed. This is likely very similar to what you mention running into with the 419 rejection and XSRF.

You can make this work with the default authentication, but it’s going to require changing your requests to behave like a queue and make sure the previous one has returned before firing the next one. This comes with it’s own downsides (speed being one) and is likely a heavier lift than implementing Passport or Sanctum with your frontend which is why I recommended that up front.

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement