Laravel Sanctum 419 Errors: A 5-Request Network Panel Triage
You set up Sanctum for your SPA, hit "Log in", and get 419 Page Expired (or a CORS error, or a 401 right after a "successful" login). Then the guessing starts: flip supports_credentials, add a domain somewhere, clear cac
You set up Sanctum for your SPA, hit "Log in", and get 419 Page Expired (or a CORS error, or a 401 right after a "successful" login). Then the guessing starts: flip supports_credentials, add a domain somewhere, clear cache, try again.
There's a faster way. Sanctum cookie auth is a chain of requests, and the browser's Network panel tells you exactly which link is broken. This is the triage order I use before touching any config.
How the chain is supposed to work
For a first-party SPA using Sanctum's cookie (stateful) mode:
- The SPA calls
GET /sanctum/csrf-cookie. Laravel sets anXSRF-TOKENcookie and a session cookie. - The SPA sends
POST /login. The HTTP client copiesXSRF-TOKENinto anX-XSRF-TOKENheader and sends the cookies. - Laravel validates the CSRF token, starts the session, and returns success.
- The SPA calls
GET /api/userwith the same cookies and gets the authenticated user.
A 419 means step 3 failed: Laravel didn't get a CSRF token that matches the session. Everything below is about finding out why.
Request 1: the preflight (OPTIONS)
Open DevTools, tick Preserve log, filter to Fetch/XHR, and log in once. If your SPA and API are on different origins, you'll see an OPTIONS request before the real one.
Check the response headers:
-
Access-Control-Allow-Originmust be your exact frontend origin (for examplehttp://localhost:3000), never*. Browsers refuse credentials with a wildcard. -
Access-Control-Allow-Credentials: truemust be present.
If either is wrong, the fix is in config/cors.php:
'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'supports_credentials' => true,
A common miss: login isn't in paths, so /sanctum/csrf-cookie works but /login gets no CORS headers at all.
Request 2: /sanctum/csrf-cookie
You want a 204 with two Set-Cookie headers: XSRF-TOKEN and your session cookie (laravel_session or your app's name).
If the cookies are set but show a yellow warning icon in the Cookies tab, the browser rejected them. Hover the icon; it usually tells you why:
-
Domain mismatch:
SESSION_DOMAINdoesn't cover the host. Usenullfor plain localhost, or.example.comwhen the SPA and API share a parent domain (app.example.comandapi.example.com). -
Secure over HTTP:
SESSION_SECURE_COOKIE=trueon a non-HTTPS local setup means the cookie is silently dropped.
Request 3: POST /login (where the 419 shows up)
Open the request headers and check two things:
-
Is
X-XSRF-TOKENpresent? If not, your client isn't reading the cookie. With Axios 1.6.2 and later, cross-origin requests need both flags:
import axios from 'axios';
export const api = axios.create({
baseURL: 'http://localhost:8000',
withCredentials: true,
withXSRFToken: true,
});
Use this one instance everywhere. A second bare axios.post() call somewhere in the codebase is a classic source of "it works on one page but not another".
-
Is the
Cookieheader sent? If the header exists but cookies are missing, go back to Request 2. The browser never stored them.
If both are present and you still get 419, Laravel probably isn't treating the request as stateful. Check the next section.
The stateful-domain check
Sanctum only applies session and CSRF middleware to requests from domains listed in SANCTUM_STATEFUL_DOMAINS. The value is a host with port, without scheme:
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=null
And in Laravel 11+ make sure stateful API middleware is enabled in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->statefulApi();
})
Two traps I see all the time:
- The frontend runs on
localhost, but the API is called via127.0.0.1. To the browser those are different sites, so cookies don't cross over. Pick one and use it everywhere. - You edited
.envbut config is cached. Runphp artisan config:clearand restart the server.
Request 4: GET /api/user
If login returns 200/204 but this returns 401, the session cookie from login isn't coming back. Usually that's the same SESSION_DOMAIN or localhost vs 127.0.0.1 mismatch, or the route is protected with auth:sanctum while the request isn't recognised as stateful.
Request 5: the one you shouldn't make
Don't "fix" 419 by adding /login to the CSRF exceptions. It hides the symptom and removes protection from the route that needs it most. If the chain above checks out, Sanctum works with CSRF on.
Quick checklist
- [ ]
Access-Control-Allow-Originis the exact SPA origin, not* - [ ]
supports_credentialsistrue, andlogin/logoutare in CORSpaths - [ ]
/sanctum/csrf-cookiesets both cookies with no browser warning - [ ]
X-XSRF-TOKENheader andCookieheader are both onPOST /login - [ ]
SANCTUM_STATEFUL_DOMAINSincludes host and port, no scheme - [ ] Same hostname everywhere (
localhostor127.0.0.1) - [ ]
php artisan config:clearafter every.envchange
If you want to sanity-check what your API actually returns for a given origin before digging into Laravel config, I built a free CORS header tester and generator for exactly that.
For the full Laravel 13 walkthrough, including different-subdomain setups, Next.js server-context gotchas, and a long FAQ, see the complete guide: Laravel Sanctum CORS 419 Error Fix (Laravel 13) on Blogs World.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.