theferrett's avatar

How Do You Retain Session/CSRF Tokens Across Multiple Browser Tabs?

So we've developed an app where our users look up orders and approve them. And they routinely right-click open several orders to approve them in a row.

The problem is, when they switch from one tab to another, their session changes - and the CSRF token used to make AJAX requests in React gets invalidated. So our whole permissions stack crashes.

Currently, we have sessions stored in Redis, with expire_on_close set to false. To make things more complicated, we have a load balancer distributing the requests across multiple pods, so there's no guarantee the virtual server a user is hitting on one request will be the server they hit on the next.

The CSRF token is both given in the header () and passed into the React app through bootstrap.js (window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';).

I've seen several "solutions" saying "set Laravel to per session," but then Googling that yields either nothing of relevance or answers for Laravel 5.5 from 2015 (though that could be my poor Google-fu).

How do we modern Laranauts handle shared authentication across multiple browser tabs? I'll happily volunteer all configuration data that I may have left out. And thanks!

0 likes
5 replies
azimidev's avatar

One solution to this issue could be to store the CSRF token in a cookie instead of in the session. This way, even if the user switches tabs, the CSRF token will still be available in the cookie and can be used to make AJAX requests. You can use the csrf_token function in Laravel to generate the token, and then store it in a cookie using JavaScript.

<meta name="csrf-token" content="{{ csrf_token() }}">
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
document.cookie = `XSRF-TOKEN=${csrfToken}`;

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;

When making an AJAX request, Laravel will automatically look for the CSRF token in the X-CSRF-TOKEN header or in the _token input value. By storing the token in a cookie, it will persist even if the user switches tabs.

In case you want to keep storing the token in the session, you may want to consider storing the session in a shared cache like Memcached or Redis, which will allow multiple instances of your application to share the same session data. This way, even if the user is directed to a different pod, they will still have the same session and the CSRF token will not be invalidated.

1 like
theferrett's avatar

@azimidev So question: How does the cookie value bubble up to the Axios defaults here? I see the cookie being stored; is there some automatic retrieval method that overrides the token being set from the document.querySelector variable?

azimidev's avatar

@theferrett the cookie is included in subsequent requests to the server, where it can be used to validate the authenticity of the request.

When you set the X-CSRF-TOKEN header on window.axios.defaults.headers.common, Axios will automatically include this header in all subsequent requests. Since the value of this header is being set to the same value as the XSRF-TOKEN cookie, the server will be able to validate the request as authentic.

theferrett's avatar

@azimidev I do get that, but the code as written doesn't necessarily do that, unless I'm misunderstanding some central aspect of Laravel's wiring.

First off, the JS sets the cookie - but it doesn't read it anywhere, so the cookie-read value never makes it to the Axios library. As written, Axios gets is a variable that's taken from the header, written to a cookie that's never used for anything (as far as I can see), and then used in Axios.

Second - and I've tested this - even if you set the cookie to a different value, it gets overwritten on the next page load when the header's read, the CSRF token is extracted, and the cookie is rewritten. And since our fundamental problem is that the CSRF is changing between tabs, if tab #1 has GOOD_XSRF and tab #2 has BAD_XSRF, the cookie never being read means that tab #2 still has a bad X-CSRF-TOKEN.

So functionally, this solution seems no different from just taking it from the header whenever the page is loaded.... which is now making me wonder whether the issue is some glitch in the addHttpCookie value in app/Http/Middleware/VerifyCsrfToken.php.

azimidev's avatar

@theferrett Yes, Laravel uses a middleware called VerifyCsrfToken to regenerate the CSRF token on each request. The point of using a token to prevent Cross-site "request" forgery is to have a different token on each request otherwise it would be pointless. Middlewares are responsible for anything to do with requests.

Please or to participate in this conversation.