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

AndrewMatthew's avatar

Sanctum SPA CSRF Token Mismatch via Postman

Hi,

As the title implies, I'm having an issue where I'm sending a request via Postman to my API, however I'm met with a 419 error. My Laravel app is on the domain "admin-api.foobar.local". Along side this, I am also developing a frontend on the subdomain "admin.foobar.local". I'd like to highlight that I've also created a new vhost under just the top level domain "foobar.local", copied all the code across and I didn't encounter this issue. This leads me to believe that the CSRF, even when on the same top level domain cannot be issued by a subdomain. Unfortunately this needs to be on its own subdomain and moving this to the top level domain is a non-negotiable.

I personally don't see anything that should be causing an issue as I've set this up time and time again, however it has always been from the top level domain and never from a subdomain which has led me to believe CSRF cannot be set from a subdomain.

Please let me know what's going on!

UPDATE:

For some reason when I comment out 'SESSION_DOMAIN=...' from my .env file, everything starts to work? This doesn't seem right to me, any insight would be welcome!

UPDATE 2:

When I uncomment out 'SESSION_DOMAIN=...', authentication work on my frontend but not in Postman, it also appears to be ignoring the Referer header when I submit on my frontend as I've tried changing the domains listed in my 'SANCTUM_STATEFUL_DOMAINS' env variable and it still allows me to submit the auth and authenticates me?

My config/settings are as follows:

.env

APP_URL=http://admin-api.foobar.local
SESSION_DOMAIN=".foobar.local"
SANCTUM_STATEFUL_DOMAINS="admin.foobar.local,admin.foobar.local:3000"

config/cors.php

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout', 'register'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['*'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
]

app/Http/Kernel.php

protected $middlewareGroups = [
    'api' => [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ]
];

routes/web.php

Route::post('/login', [AuthenticatedSessionController::class, 'store'])
    ->middleware(array_filter([
        'guest:'.config('fortify.guard'),
        $limiter ? 'throttle:'.$limiter : null,
    ]));

Postman request: POST http://admin-api.foobar.local/login

Prerequest Script

pm.sendRequest({
    url: "http://admin-api.foobar.local/sanctum/csrf-cookie",
    method: 'GET',
}, function (err, response) {
    const cookieJar = pm.cookies.jar();    
    cookieJar.get('.foobar.local', 'XSRF-TOKEN', (error, cookie) => {
        if (error) { 
            console.log(error);
        }
        if (cookie) {
            pm.collectionVariables.set('xsrf-cookie', cookie);
            console.log(cookie);
        }
    })
});

Body

{
    "email": "[email protected]",
    "password": "Foobar123!"
}

Headers:

Referer: http://admin.foobar.local
Accept: application/json
X-XSRF-TOKEN: {{xsrf-cookie}}

Thanks for your help!

0 likes
10 replies
localpathcomp's avatar

make request to /sanctum/csrf-cookie, it will return a non-http only cookie named XSRF-TOKEN Copy the value of that cookie (excluding the time and meta-data, make sure the %3d is not there either) and place it in a header in your Postman named X-XSRF-TOKEN. Your request now has csrf protection and you should be able to access any routes protected by that middleware.

XSRF-TOKEN=eyJpdiI6InpkWFlqWUFsSFc4STQ3Ri9abktyV2c9PSIsInZhbHVlIjoiaG1INjMvYmhvaG5ObG1rQm1JNFZLOTR4QWRRMzBvM25vQmRCYlFqejdFUzlEWTM0Z3R6Si9aSlp1S3RQUkY1ajVSemNGeTdRVjZvdzZPYlR4Wk9WQUdsbTJoSkg4YmVaTjVjOExqbU04VHJTQmVKMy8yaE1SWkQxYUVqSVFBNjgiLCJtYWMiOiIxMzgzZjMxNTQ0NDlhOGY4NzczZGQwZTg5NzNjZDRkYmQwNGZjNzJhMmVlMmFmODUwMjg1ZjlkZDk3NWI3NjIzIiwidGFnIjoiIn0%3D; Path=/; Domain=thedeka.test; Secure; Expires=Sat, 22 Jan 2022 15:44:17 GMT;

Just add the the header in postman equal to this part: eyJpdiI6InpkWFlqWUFsSFc4STQ3Ri9abktyV2c9PSIsInZhbHVlIjoiaG1INjMvYmhvaG5ObG1rQm1JNFZLOTR4QWRRMzBvM25vQmRCYlFqejdFUzlEWTM0Z3R6Si9aSlp1S3RQUkY1ajVSemNGeTdRVjZvdzZPYlR4Wk9WQUdsbTJoSkg4YmVaTjVjOExqbU04VHJTQmVKMy8yaE1SWkQxYUVqSVFBNjgiLCJtYWMiOiIxMzgzZjMxNTQ0NDlhOGY4NzczZGQwZTg5NzNjZDRkYmQwNGZjNzJhMmVlMmFmODUwMjg1ZjlkZDk3NWI3NjIzIiwidGFnIjoiIn0

10 likes
usanzadunje's avatar

@localpathcomp Thanks. I've started losing my mind why this didn't work. Because I blindly copy pasted token value with %3d. After removing that from token everything is working fine.

Thanks again!

3 likes
T620's avatar

@usanzadunje This happened to me too. If you're testing in Insomnia, copy the value of the cookie manually and set the header X-XSRF-TOKEN to the value you copied. Your token should not have the %3d. In my case, my token ended in =0.

isimmons's avatar

@localpathcomp Is this a bug in Laravel? I have the same issue in a simple SPA I'm creating using fetch instead of axios and I had to manually remove this %3D from my xsrf token. To test I just used token.slice(0, -3) and it works but I wonder should I explicitly look for '%3D' and remove any number of them? Is it possible that I'll get a token at some point with '%3D%3D' or more?

Thanks for the initial solution by the way

isimmons's avatar

So I found some info since I finally knew what to google for.

It's not a Laravel bug but more of a "If you don't use axios you're on your own" sort of thing. Apparently axios handles this for us automatically when it automatically sets the the xsrf token header . https://github.com/nuwave/lighthouse/issues/1406

Much better than simply removing the last 3 characters is using decodeURIComponent https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent

kachi_dk's avatar

This worked for me. After adding this to the .env

SESSION_DOMAIN=http://localhost:XXXX
SANCTUM_STATEFUL_DOMAINS=http://localhost:XXXX

I ran this

php artisan config:cache
2 likes
yaroofie's avatar

I had the same problem and I fixed it like this :

add this to your .env

SESSION_DOMAIN=yourdomain.com
SANCTUM_STATEFUL_DOMAINS=yourdomain.com
SESSION_SECURE_COOKIE=false

then add your domain to cookie section of postman Cookie section then Add your domain then hit add domain after that you can [optional] add your postman envirement variables Add postman env variables then with a simple post request scripting you can save the xsrf-token cookie value as a postman envirement variable like this save xsrf-token to env variable don't worry I won't make you type it out:

pm.test("Setting xsrf_token env vairable", function () {
    let cookie = pm.response.cookies.get('XSRF-TOKEN');
    if(cookie) pm.environment.set('xsrf-token',cookie);
    else {
        let header = pm.response.headers.find(h => h.key == 'Set-Cookie' && h.value.startsWith('XSRF-TOKEN'));
        cookie = header?.value.substring(header?.value.indexOf("=") + 1, header?.value.indexOf("%3D"));
        pm.environment.set('xsrf-token',cookie);
    }
});

then on your web routes you must add x-xsrf-token header add-x-xsrf-token-header

this should work ,at least it works for me, I hope it helps you, peace out!

3 likes

Please or to participate in this conversation.