Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

IlyaBuldakov's avatar

Laravel Sanctum generates new XSRF-TOKEN for stateful API Requests

I use Laravel Sanctum to authorize SPA application requests (NuxtJS). In general, I understood the idea with the /sanctum/csrf-cookie endpoint and am already sending a request using axios, which installs the XSRF TOKEN once before the user logs in, and then uses it for API methods while the session is alive and the XSRF-TOKEN itself is valid.

But I get the following result: Laravel generates a Set-Cookie XSRF-TOKEN header with each API request that I send before I go to /sanctum/csrf-cookie. For example: when the main page loads, 4 requests are sent (to receive data from the backend for rendering content). So each response to this request has a Set-Cookie XSRF-TOKEN header and overwrites it each time.

At the same time, the Set-Cookie response headers /sanctum/csrf-cookie look strange: it not only returns a new XSRF-TOKEN (as all API requests do for some reason), but also returns some_uuid=something similar to XSRF-TOKEN.

My front-end and backend work locally in docker-compose, so SANCTUM_STATEFUL_DOMAINS=localhost (although I tried to specify different values there, it didn't help).

I also tried specifying the FRONTEND_URL and changing the session settings. By the way, here they are (listed by "/" what I tried):

SESSION_DRIVER=cookie/redis/database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null/localhost

I use Laravel 11 and in my bootstrap/app.php specified by $middleware->statefulApi();; routes I tried has "api" middleware; axios config has withCredentials: true, withXSRFToken: true

What could be the problem and how to reduce the overhead of the backend (useless generation of a bunch of tokens for each request), as well as eliminate the problems with token synchronization on the front-end that this problem may cause?

Thank you in advance for your help.

0 likes
18 replies
jlrdw's avatar

There are three sections to Sanctum code depending on what you are doing. If it's an API, follow that section.

IlyaBuldakov's avatar

@jlrdw I'm making a stateful API (cookie-based authorization API). I don't need any tokens from Sanctum at the moment.

IlyaBuldakov's avatar

Guys, I really think this question needs an answer, as the community is at a loss, including me.

Take a look at these forum threads that don't have answers on the case (solutions to the problem):

This one

And this too

Another one

...and others

Just look at how many people have written under these topics that have the same unresolved issue.

1 like
IlyaBuldakov's avatar

@jlrdw As far as I understood the user's problem from the topic above, his application is not a SPA and the problem is to combine the standard mechanism for the non-SPA part of the site (Laravel's boxed solution - XSRF-TOKEN for each request) and Sanctum. In his case, you really shouldn't disable the main CSRF protection mechanism.

In my case, I want to fully tell Laravel that my front-end is an SPA and there is no need to update the XSRF TOKEN with each API request, since this happens within the same session of interaction with the SPA application and it is still the same user.

That's why I use Sanctum - my authorization functionality remains "laravel-in-box" cookie-based, but Sanctum provides me with SPA CSRF protection, which, for some reason, does not want to be friends with the main one.

jlrdw's avatar

@IlyaBuldakov Have you checked if there are any issues on github concerning this.

4 requests are sent (to receive data from the backend for rendering content).

But what are in the request.

  • Same request repeated
  • Different data in the 4 request

Also is routing setup correctly. Also check CORS.

IlyaBuldakov's avatar

@jlrdw No, the requests are not repeated, the frontend sends 4 different requests to get a list of cities, products, articles from the API, and so on.

The data itself that I need is fine. But each request generates an XSRF-TOKEN overwrite, since it contains an XSRF-TOKEN Set-Cookie. That's my problem.

I wanted to post an issue on GitHub, but was redirected to the Laracasts Forum, so here I am.

jlrdw's avatar

@IlyaBuldakov

But each request generates an XSRF-TOKEN overwrite, since it contains an XSRF-TOKEN Set-Cookie.

That seems like normal behavior.

In a regular web app it's not duplicated because session is used. An API is stateless.

IlyaBuldakov's avatar

@jlrdw I agree with you that updating the XSRF token is justified in a regular web application, but then why does Sanctum provide the /sanctum/csrf-cookie method? The documentation says that I have to request the XSRF token once and it will be used for all requests to my API, but the standard Laravel behavior somehow updates my token every time - it's pointless.

IlyaBuldakov's avatar

@jeffallen Hi. I think stackoverflow user's question is not really about my topic. His question is about that there is no need to check for an XSRF token if you have already sent a request for a /sanctum/csrf-cookie.

Indeed, if you have sent a request for a /sanctum/csrf-cookie, the browser will automatically set a token thanks to the Set Cookie header of the response. And this is the correct behavior - my app also automatically installs this token.

But resentment arose due to the fact that /sanctum/csrf-token makes no sense, because any API request automatically passes CSRF authorization by sending a Set Cookie header in response. Then what is the meaning of the /sanctum/csrf-cookie endpoint? It seems to me that overwriting the XSRF token every request is incorrect behavior.

JussiMannisto's avatar
Level 50

@IlyaBuldakov

any API request automatically passes CSRF authorization by sending a Set Cookie header in response.

Wrong. Routes in the web group (and stateful api routes) do use CSRF protection, but only for state-altering methods such as POST and DELETE, not "safe" methods such as GET.

Laravel includes the token in a cookie called XSRF-TOKEN so that front-end HTTP libraries like Axios can automatically discover it and add it as the X-XSRF-TOKEN header in future requests.

3rd party sites can't read the cookie and thus can't add the header. This is due to same-origin policy, which is also the reason why GET requests don't need to be protected against CSRF: a 3rd party site can't read the response. They also can't read POST, DELETE, etc. responses, but those are unsafe actions and must be blocked altogether.

You could, in theory, call any stateful GET route to get the token cookie prior to sending any unsafe requests. It just makes sense to use a route made specifically for that purpose.

As to why you get a different token each time, I'm not sure. You should get the same token every time as long as the session cookie is present on each request and you're not doing something that regenerates the session (e.g. login or logout). The token is just a single string in session, regenerating it has no actual overhead, and session data is written out at the end of each request anyway. So it won't affect performance.

P.S. Did I understand correctly that you receive a different token on every request? If you get the same token each time, then everything's working correctly. It's just a response header.

P.P.S. I think I see what's going on. Laravel encrypts cookies, so you should see a different set-cookie value on every request, even when the token doesn't change. If you're worried about the cookie being included in every response, and only want it done in the Sanctum endpoint, you can extend the VerifyCsrfToken middleware and customize it instead of using the off-the-shelf version. See this method in particular. But this is a total non-issue in my opinion.

2 likes
IlyaBuldakov's avatar

@JussiMannisto Hi. Thanks for the explanation. I understand exactly how Axios clients work with cookies that Laravel writes to the browser, and you're right that that's not my question. I'm just trying to find out why I get different tokens when I have a flat idea in my head (from docs) - Once go to /sanctum/csrf-cookie -> browser writes XSRF-token in cookie -> axios uses it for CSRF-authorizing SPA API requests. That's it!

Instead, I get a behavior that gives me a new XSRF token for each request.

And again, you're right when you say that cookies are encrypted. But then I have a question - how do other people use Sanctum? Isn't it strange that I'm sending a request for a /sanctum/csrf cookie, implying that I'm going to use it for a long time until the user decides to refresh the page or terminate the session, but in reality the token I generated doesn't make sense because it gets overwritten?

Okay, let's assume that this is how it should be. Then why do I need Sanctum? (Without considering the API token management functionality. As the documentation says, Sanctum has two modes, and we are talking about the SPA mode now.)

I thought it provided a more convenient solution to protect stateful API (SPA) applications so as not to experience problems with XSRF token desynchronization on the front end, but in fact it turns out that it is easier for me to remove it from dependencies using the standard Laravel CSRF mechanism, agreeing to overwrite the XSRF token with each request?

JussiMannisto's avatar

@IlyaBuldakov

Okay, let's assume that this is how it should be. Then why do I need Sanctum?

You'd use it for a SPA when you need a) server-side sessions b) CSRF protection. For those you'll need a way of acquiring a CSRF token. If you have a stateful API and try to make a POST request without a CSRF token, you'll get a 419 error.

You're probably sending other GET requests which also return the token cookie. The /sanctum/csrf endpoint is just a dedicated route for retrieving it.

(...) implying that I'm going to use it for a long time until the user decides to refresh the page or terminate the session, but in reality the token I generated doesn't make sense because it gets overwritten?

It shouldn't get overwritten. Have you verified on the backend that the decrypted token value actually changes? Laravel uses AES for encryption, and every time the token gets encrypted, a different IV is used, resulting in a different XSRF-TOKEN cookie value.

1 like
IlyaBuldakov's avatar

@JussiMannisto Thanks for the reply. What I did:

  1. Run php artisan tinker
  2. Crypt::decryptString(*my-xsrf-token*) (repeat step 2 for a couple of tokens I receive)
  3. Checked that the output is the same

In that case, I may not use Sanctum at all if I don't use API tokens. You're talking about service-side sessions and CSRF protection - all of that is provided by the "web" middleware, right?

JussiMannisto's avatar

@IlyaBuldakov If you have an SPA and want to drop Sanctum, how do you plan to protect against CSRF?

The web middleware stack works when you use something like Inertia, since you're hitting Laravel's routes for the pages and receive the XSRF-TOKEN cookie in those responses. When you have a pure SPA using Laravel as an API, you don't have that luxury. That's where Sanctum becomes useful.

Both the web middleware and stateful APIs protect against CSRF, but you need a way of acquiring a CSRF token first.

I only use Sanctum for API access tokens, but that's because I use Inertia nowadays.

1 like

Please or to participate in this conversation.