shaneomac's avatar

CSRF - Why is it so painful? Laravel 12 w React/Inertia

can anyone add some input as how to best handle CSRF throughout a Laravel/React app?

It was immensely painful to figure out with partial and occasional failures. Different ENVs failed at different rates.

From our app to Stripe and back was a common 419 error. Our checkout session is excluded from CSRF to make it seamless.

I have since mitigated almost entirely with a React Util to refresh tokens and grab it before hitting controllers and such.

I tried so many different ways and they all had different rates of success.

We are now in production with a small user base and I still see 419 errors in NightWatch for one specific part of the app. Normally after one failure they get through.

But I can't seem to find any info online. It's all 5-7 years old.

Keep in mind I am not traditionally trained. I am self taught and rely on AI for writing the code but I can architect and build just about thing and I am quite good at reading and understanding code and catching the flaws of AI.

So I have my gaps.

But any info into the modern Laravel 12 with React handling CSRF PERFECTLY, with no failures in the app, would be great.

I feel we are smooth sailing once we get this one area worked out.

A dev helping me has a vast training but is just as stuck as I am am when it comes to CSRF.

Anything that may be remotely helpful, please chime in!

0 likes
9 replies
LaryAI's avatar
Level 58

Certainly! Handling CSRF in a Laravel 12 backend with a React/Inertia frontend can be tricky due to the split between server-rendered (Blade) and SPA architecture, but it can definitely be robust and reliable. Here’s a step-by-step approach and some key tips to make it “just work” — including troubleshooting persistent 419 (CSRF Token Mismatch) issues.

1. Understanding the CSRF Mechanism

Laravel protects against CSRF with tokens stored in user sessions and validated on POST/PUT/PATCH/DELETE requests. SPA architectures push challenges since you lose Laravel’s automatic Blade-generated CSRF token injection.

2. How CSRF Works With Inertia + React

Inertia stays server-driven, but you’re responsible for making sure every axios/fetch call includes a valid CSRF token. This is especially true when logging in, logging out, or performing form submits.

3. Best Practice Implementation

A. Expose the CSRF Token to React

Upon loading the initial page, inject the CSRF token into your React app. The most reliable ways:

Via an endpoint:

Define in routes/web.php:

Route::get('/csrf-token', function () {
    return response()->json(['csrfToken' => csrf_token()]);
});

From React, fetch it before making POST/PUT/DELETE requests:

// Example using fetch
const getCsrfToken = async () => {
  const response = await fetch('/csrf-token', { credentials: 'include' });
  const data = await response.json();
  return data.csrfToken;
};

Or, if using Inertia on initial load: Pass it as a prop to your React root component via your controller:

return Inertia::render('App', [
    'csrf_token' => csrf_token(),
]);

B. Include CSRF Token in Requests

With axios (which many use in React):

import axios from "axios";

axios.defaults.withCredentials = true;

// This should run at app start or before any secure request
axios.get('/sanctum/csrf-cookie');

// Before POST/PUT/DELETE:
axios.post('/api/endpoint', formData, {
  headers: {
    'X-CSRF-TOKEN': csrfToken
  }
});

For fetch:

fetch('/api/endpoint', {
  method: 'POST',
  headers: {
    'X-CSRF-TOKEN': csrfToken,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(payload),
  credentials: 'include',
})

C. Watch For Caching & Multiple Environments

Ensure your environments (prod, staging, local) are all consistent with:

  • Same domain/subdomain (or configure SESSION_DOMAIN)
  • HTTPS sessions if needed (SESSION_SECURE_COOKIE)
  • No caching of responses containing CSRF tokens

D. Dealing With Stripe or Other Externals

When Stripe POSTs back, do not CSRF-protect their callback endpoints. Add them to VerifyCsrfToken::$except in app/Http/Middleware/VerifyCsrfToken.php:

protected $except = [
    'stripe/webhook',
    // any other external callback
];

Do not disable CSRF globally or for your entire checkout flow if you have internal-facing requests, only for webhooks or API endpoints hit from external vendors.

E. Auto-Refreshing The CSRF Token

If your session/cookie expires often, you can set up a React/axios interceptor to fetch a fresh token when a 419 is detected:

axios.interceptors.response.use(null, async (error) => {
  if (error.response && error.response.status === 419) {
    await axios.get('/csrf-token');
    // Optionally retry the original request here
  }
  return Promise.reject(error);
});

However, if users keep getting a 419 after refresh, it’s a sign of:

  • Session misconfiguration (domain, HTTPS, cookie path, etc.)
  • Requests not consistently passing credentials/cookies
  • Proxy or load balancer not configuring cookies correctly

F. Nightwatch/Testing

E2E test runners sometimes fail if they don’t persist cookies/session or start with a fresh context. Always mimic a real user flow by visiting the main app before submitting POST requests.

4. Summary Checklist

  • Always send X-CSRF-TOKEN header in API requests (React side), fetched from /csrf-token or as a prop.
  • Never CSRF-protect Stripe/external webhooks.
  • Ensure SESSION_DOMAIN, SESSION_SECURE_COOKIE, etc. are set for your needs.
  • Handle 419 by auto-refreshing the CSRF token and retrying once.
  • E2E/browser tests need to warm up session/cookies first.
  • Do not cache responses with CSRF tokens.
  • Test login/logout/checkout across all environments for domain/session consistency.

Modern Example:

// React: At app start
useEffect(() => {
  axios.get('/csrf-token').then(r => {
    axios.defaults.headers.common['X-CSRF-TOKEN'] = r.data.csrfToken;
  });
  axios.defaults.withCredentials = true;
}, []);

Verify session config (.env):

SESSION_DOMAIN=.yourdomain.com
SESSION_SECURE_COOKIE=true

5. References

If you need more detailed troubleshooting, share your session/cookie ENV settings and specific flow that triggers 419s and I can help get more specific!

JussiMannisto's avatar

There should be no CSRF protection issues if you use Inertia. It works out of the box. If you have errors in a specific part of your app, you should go into more detail there.

Keep in mind I am not traditionally trained. I am self taught and rely on AI for writing the code

Do you have an understanding of security basics: why are CSRF tokens used, which routes are subject to CSRF protection and why, how cookies work, etc.?

I have since mitigated almost entirely with a React Util to refresh tokens and grab it before hitting controllers and such.

There should be no need for this unless you have a very short session lifetime. Whenever you make a backend request (e.g. a page load), Laravel includes an XSRF-TOKEN cookie in the response. JS clients such as Axios (used by Inertia) automatically read the token and include it in all subsequent requests as a header.

shaneomac's avatar

Thanks for the reply!

I do! But ensuring it is working everywhere has been tough. I get why it exists.

Is CSRF token in the global Inertia props by default?

So should I just use Axios for all backend requests?

JussiMannisto's avatar

Is CSRF token in the global Inertia props by default?

No. It's included in the XSRF-TOKEN cookie like I mentioned.

So should I just use Axios for all backend requests?

You can send requests however you like, as long as you include a valid X-CSRF-TOKEN header in "unsafe" (non-GET) requests. Axios automatically reads the cookie and adds the header.

If you're sending cross-origin requests, you may have to add additional configuration to Axios, i.e. withCredentials and withXSRFToken. But you didn't mention that, so I assume all requests are to the same origin.

shaneomac's avatar

@jussimannisto

Any chance this in the app.blade.php could be the culprit?

" " I found this in the Inertia docs "Laravel automatically includes the proper CSRF token when making requests via Inertia or Axios. However, if you're using Laravel, be sure to omit the csrf-token meta tag from your project, as this will prevent the CSRF token from refreshing properly."

Looks like it snuck into our app 8 months ago when AI told me it would fix some ongoing 419 errors.

Look at other files in this commit, I was using Fetch for the requests.

It's all making sense in hindsight but also really frustrating the dev working on this is not able to quickly diagnose and troubleshoot this.

JussiMannisto's avatar

Passing the CSRF token in HTML works in a traditional multi-page application because the entire page is reloaded on each request, and an up-to-date CSRF token is rendered each time. That's not the case with single-page applications, where the main HTML is rendered exactly once. That's why Laravel transmits the fresh CSRF token in a cookie.

So yes, the docs are correct. If you keep using the CSRF token from the initial HTML, and the token is regenerated on the back end, the old CSRF token is invalid and you'll get 419s.

LLMs are absolute trash when it comes to things like this. Consult them if you want - they may even give you ideas on what to look into. But never take their suggestions at face value, because they're constantly wrong, or just hallucinate things.

janum's avatar

I have been through this exact pain with Laravel, Inertia and React, and most of the old advice online does not fully apply to Laravel 12 anymore. The main issue I learned is that CSRF failures rarely come from Laravel itself. They almost always come from token desync, caching, or a missing handshake somewhere between the first page load and the actual request.

Here are the things that finally gave me a completely stable setup with zero 419 errors:

1. Never rely on a manually refreshed CSRF token in React The React util works, but it is only a band-aid. The correct flow is to let Laravel handle token rotation and just call: GET /sanctum/csrf-cookie before any authenticated request. Inertia automatically sends the XSRF-TOKEN cookie as long as it exists.

2. Make sure nothing is caching the HTML that contains the CSRF token I had random failures only in production due to Cloudflare caching the initial Inertia response for a few minutes. The CSRF cookie was fresh, but the client was rendering an old Inertia page that referenced an expired token. That caused the “occasional 419” effect. Solution: Disable page caching for all Inertia routes or at least bypass CSRF cookies.

3. Confirm that your Stripe redirect is excluded correctly Redirects from external domains will always break the CSRF cookie unless you exclude the callback route. Laravel 12 uses the VerifyCsrfToken middleware the same way as older versions. Make sure the Stripe return callback and any interim webhooks are in that $except array.

4. Check for SameSite issues on Chrome One of my weirdest 419 cases was caused by Chrome treating the XSRF-TOKEN cookie as SameSite=Lax during a redirect. Setting it explicitly to SameSite=None; Secure stabilised everything:

Session::setCookieParameters([
    'same_site' => 'none',
    'secure' => true
]);

5. For Inertia, always make the first request visit /sanctum/csrf-cookie A lot of devs forget this step. Once the cookie is set, the CSRF header is handled automatically. No need to manually fetch the token at all.

6. Nightwatch failures usually mean “token rotated between steps” If the only failures now appear in Nightwatch, not in real users, then the test runner is hitting the app too quickly and invalidating its own token sequence. I had this happen as well. Adding a short wait before form submission fixed my false positives.

After fixing these pieces, my app stopped producing 419 errors completely, even under load.

CSRF in Laravel 12 is actually very stable; the tricky part is understanding how React, Inertia and external redirects interact with the cookie. Once you lock those flows down, it becomes predictable.

Hopefully some of this helps, because I know exactly how frustrating the random failures can be.

JussiMannisto's avatar

Speaking of LLMs being wrong:

  1. For Inertia, always make the first request visit /sanctum/csrf-cookie A lot of devs forget this step. Once the cookie is set, the CSRF header is handled automatically. No need to manually fetch the token at all.

This is pointless with Inertia. The initial Inertia page response already includes the cookie. You'd only need this step if you had a pure SPA with Laravel as a backend API.

1 like
shaneomac's avatar

@janum Thanks for all that insight!

@jussimannisto It is all become super clear the last few days for me where I have gone wrong. I just wish I knew all this info months ago but never again will I struggle.

Appreciate your responses!

Please or to participate in this conversation.