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-TOKENheader in API requests (React side), fetched from/csrf-tokenor 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!