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

RJF's avatar
Level 13

Sanctum with multiple domains and multiple subdomains: how to configure?

Until yesterday I had a pure API Laravel application successfully supporting two frontend applications. The frontend applications are not built from Laravel. Both frontends are on different domains and different from the backend too. The token-based Sanctum authentication was working fine.

Now I have added a third frontend application (cloned off one of the existing frontend applications) with its own (fourth) domain. This is not working fine.

Problem: in the new domain, Laravel returns success 204 to sanctum/csrf-cookie but Laravel’s response does not contain any Set-Cookie header for XSRF. Weirdly, this problem only exists when the request URL has no subdomain. When I add any random subdomain , everything works fine even though I get no Set-Cookie header in the response.

My case - multiple domains and multiple subdomains - seems not to be covered by the docs.

I have run php artisan cache:clear and php artisan config:clear

What’s happening?

+--------------------------------------+----------------------------------------------+
|                                      | Laravel responds with Set-Cookie: XSRF-TOKEN |
+--------------------------------------+----------------------------------------------+
| my-backend-domain.com                |                                              |
+--------------------------------------+----------------------------------------------+
| my-frontend-domain1.com              | YES                                          |
+--------------------------------------+----------------------------------------------+
| anysubdomain.my-frontend-domain1.com | YES                                          |
+--------------------------------------+----------------------------------------------+
| my-frontend-domain2.com              | YES                                          |
+--------------------------------------+----------------------------------------------+
| anysubdomain.my-frontend-domain2.com | YES                                          |
+--------------------------------------+----------------------------------------------+
| my-frontend-domain3.com              | NO                                           |
+--------------------------------------+----------------------------------------------+
| anysubdomain.my-frontend-domain3.com | NO, but it works OK anyway                                      |
+--------------------------------------+----------------------------------------------+

config/sanctum.php

(the .env file has no value)

'stateful' => explode(',', env(
        'SANCTUM_STATEFUL_DOMAINS',
        'http://localhost,localhost,localhost:3000,localhost:8004,127.0.0.1,127.0.0.1:8000,::1,my-frontend-domain1.com,my-frontend-domain2.com,my-frontend-domain3.com 
    )),

config/session.php

(the .env file has no value)

'domain' => env('SESSION_DOMAIN', null),

config/cors.php

    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

Headers for my-frontend-domain2.com (working fine)

Request URL: https://www.my-frontend-domain1.com/sanctum/csrf-cookie
Request Method: GET
Status Code: 204 No Content
Remote Address: (address redacted)
Referrer Policy: strict-origin-when-cross-origin

REQUEST

GET /sanctum/csrf-cookie HTTP/1.1
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,ja;q=0.7,zh;q=0.6
Cache-Control: no-cache
Connection: keep-alive
DNT: 1
Host: www.my-frontend-domain2.com
Pragma: no-cache
Referer: https://www.my-frontend-domain2.com/login
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"

RESPONSE

HTTP/1.1 204 No Content
Server: nginx/1.18.0
Date: Sat, 11 Mar 2023 11:43:08 GMT
Connection: keep-alive
Cache-Control: private, must-revalidate
pragma: no-cache
expires: -1
Vary: Origin
Set-Cookie: XSRF-TOKEN=(330 characters redacted); expires=Sat, 11-Mar-2023 13:43:08 GMT; Max-Age=7200; path=/; samesite=lax
Set-Cookie: dbname_session=(330 characters redacted); expires=Sat, 11-Mar-2023 13:43:08 GMT; Max-Age=7200; path=/; httponly; samesite=lax
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff

Headers for laracastsiscool.my-frontend-domain3.com (working fine)

Request URL: https://laracastsiscool.my-frontend-domain3.com/sanctum/csrf-cookie
Request Method: GET
Status Code: 200 OK
Remote Address: (redacted)
Referrer Policy: strict-origin-when-cross-origin

REQUEST

GET /sanctum/csrf-cookie HTTP/1.1
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,ja;q=0.7,zh;q=0.6
Cache-Control: no-cache
Connection: keep-alive
DNT: 1
Host: laracastsiscool.my-frontend-domain3.com
Pragma: no-cache
Referer: https://laracastsiscool.my-frontend-domain3.com/login
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"

RESPONSE

HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sat, 11 Mar 2023 12:37:32 GMT
Content-Type: text/html
Content-Length: 4961
Last-Modified: Fri, 10 Mar 2023 17:09:44 GMT
Connection: keep-alive
ETag: "640b6458-1361"
Accept-Ranges: bytes

Headers for my-frontend-domain3.com (fails when no subdomain)

Request URL: https://my-frontend-domain3.com/sanctum/csrf-cookie
Request Method: GET
Status Code: 200 OK
Remote Address: (redacted)
Referrer Policy: strict-origin-when-cross-origin

REQUEST

GET /sanctum/csrf-cookie HTTP/1.1
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,ja;q=0.7,zh;q=0.6
Cache-Control: no-cache
Connection: keep-alive
Cookie: dbname_session=(330 characters redacted)
DNT: 1
Host: my-frontend-domain3.com
Pragma: no-cache
Referer: https://my-frontend-domain3.com/login
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"

RESPONSE

HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sat, 11 Mar 2023 11:41:30 GMT
Content-Type: text/html
Content-Length: 4961
Last-Modified: Fri, 10 Mar 2023 17:09:44 GMT
Connection: keep-alive
ETag: "640b6458-1361"
Accept-Ranges: bytes
0 likes
7 replies
LaryAI's avatar
Level 58

The issue seems to be related to the SESSION_DOMAIN configuration. When the request URL has no subdomain, Laravel is not setting the Set-Cookie header for XSRF.

To fix this, try setting the SESSION_DOMAIN value in your .env file to the root domain of your application (e.g. SESSION_DOMAIN=my-frontend-domain3.com).

If that doesn't work, you can try setting the SESSION_DOMAIN value to null in your .env file and then updating your config/session.php file to include the following:

'domain' => env('SESSION_DOMAIN', null),
'secure' => env('SESSION_SECURE_COOKIE', false),
'http_only' => true,
'same_site' => 'lax',

This should ensure that the XSRF cookie is set correctly for all domains and subdomains.

Also, make sure that the SANCTUM_STATEFUL_DOMAINS configuration in your config/sanctum.php file includes all the domains and subdomains that you want to allow access to your API.

'stateful' => explode(',', env(
    'SANCTUM_STATEFUL_DOMAINS',
    'http://localhost,localhost,localhost:3000,localhost:8004,127.0.0.1,127.0.0.1:8000,::1,my-frontend-domain1.com,my-frontend-domain2.com,my-frontend-domain3.com'
)),

After making these changes, clear your cache and try again. The XSRF cookie should now be set correctly for all domains and subdomains.

php artisan cache:clear
php artisan config:clear
RJF's avatar
RJF
OP
Best Answer
Level 13

Solved. But the solution to this problem was as mysterious as its appearance.

I need to support unlimited subdomains, so I needed some kind of wildcard subdomain solution.

I set SESSION_DOMAIN to null in config/session.php as suggested by the Laracasts AI, but that had no effect, so I removed the setting again.

The solution was to prefix the domain with a dot, which seems to act as a wildcard.

.my-frontend-domain3.com

This is still mysterious to me, because I had two domains configured exactly the same but the problem appeared only in one domain.

I am not satisfied with this solution because it makes no sense to me. I will post here again if I discover a more reliable explanation of the csrf problem.

vincent15000's avatar

@RJF In your post, you mention that you have multiple domains.

Have you managed to share the session between multiple domains with Sanctum ?

I have the same question, but it's clear in the documentation : to get Sanctum work, the front and the back have to be in the same domain.

RJF's avatar
Level 13

@vincent15000 We are not sharing sessions. I eventually succeeded by adding the frontend domains to the 'stateful' array in config/sanctum.php

simonhamp's avatar

The only method I could get working was if SANCTUM_STATEFUL_DOMAINS includes both the main domain (mydomain.com) and a wildcard-prefixed version (*.mydomain.com)

So

SANCTUM_STATEFUL_DOMAINS=mydomain.com,*.mydomain.com

That way I can call the backend from any subdomain and no subdomain. No other combination worked for me to achieve working with root domain and all subdomains

2 likes
somenet77's avatar

@simonhamp What is the solution if the backend and frontend are in two different subdomains like this

frontend.example.com = frontend

backend.example.com = backend

2 likes

Please or to participate in this conversation.