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

Talinon's avatar
Level 51

Using Sanctum on multiple subdomains with XSRF-TOKEN cookie

Hey all. I have been working on a mega project for months, which is the primary reason for my inactivity in the forums. I encountered a problem today that I've been spinning my wheels on for hours, so I hope someone can point out where I've gone wrong.

Laravel API backend with Sanctum. Vue SPA front end with Axios.

I have 3 applications. They all share the same top level domain. Let's call it foo.com

App #1: (main office employee app and API behind a firewall)

foo.com (client)

api.foo.com

App #2: (public API and app for field employees)

field.foo.com (client)

api.field.foo.com

App #3 (public customer portal)

portal.foo.com (client)

api.portal.foo.com

App #1 and #2 share the same session cookie using Sanctum authentication. No problems here.

App #3 has its own set of users and session configuration. It is configured to have its own Sanctum stateful (sub)domain and basically be a stand-alone application.

Here is what I observe happening. When logging into App #3, the client makes its request to the sanctum/csrf-cookie endpoint and gets the token in the response. But, when the client sends the subsequent login post request (or any other post request), it is not sending the XSRF-TOKEN for the portal.foo.com subdomain. Instead, it is sending the top-level foo.com XSRF-TOKEN cookie (set by one of the other apps). This obviously leads to a CSRF mismatch exception being thrown. (for the sake of completeness, if the other two apps were never visted, then app #3 would work fine. The problem is conflicting cookies)

If I set the SESSION_DOMAIN in App #3 to .portal.foo.com, the CSRF exception is thrown at login regardless. If I change it to .foo.com, (like the other 2 apps are configured) then user can login and the app is fully functional, BUT.. this can lead to problems if a user is concurrently using App #3 with the other apps, as they will be sharing the top level XSRF-TOKEN cookie among them. For example, A request to App #1 would set the cookie value, then a subsequent request in App #3 would overwrite it. Then if the user makes a post in App #1, it'll cause a CSRF mismatch. To be clear, this wouldn't be a problem in the production environment, as App #3 will be on its own top level domain. But, this is in a staging/sandbox environment, where I'm trying to run all 3 apps under the same server and domain to minimize provisioning extra servers.

Do I have something misconfigured on the backend? Is Sanctum designed to handle this kind of set up? I don't believe its CORS related, as that would be an error before it gets to the CSRF mismatch exception. Is it the front-end? Is it because Axios is sending the wrong XSRF-TOKEN cookie from the top level domain instead of the subdomain?

I've been using Sanctum with App #1 and #2 for months without issues. Basically, all I want to do is run this stand-alone #3 app on another subdomain without it conflicting with cookies. There must be something simple that I'm overlooking.

Any guidance would be appreciated. Thanks in advance for your time.

0 likes
6 replies
Talinon's avatar
Level 51

Prefixing the cookie works for the session cookie, but not for the XSRF-TOKEN.

Sanctum is clever, but I don't think it can properly handle this kind of set up, which is why the documentation makes it clear that they the must share the same top-level domain. I thought using multiple sub-domains would work, and it does, except for this problem.

I guess I can live with just disabling the Sanctum CSRF verification middleware for the staging environment. Although, who knows what requirements will come along in the future where I'd want to have other Sanctum-powered apps on the same domain within production. I guess I could write my own CSRF middleware and go out of my way to make sure the client includes it. I just wish there was a better way, and I find it difficult to believe there isn't. I'm open to any other ideas.

Talinon's avatar
Level 51

It looks like Sanctum DOES set the XSRF-TOKEN cookie on the proper subdomain. The problem seems to be Axios's withCredentials default that reads from the TLD. If I set up a request interceptor that reads the proper subdomain cookie and overrides the X-XSRF-TOKEN header, it appears to work.

I need to do some further testing, but it does seem like Sanctum is capable of handling multiple apps on different subdomains.

Talinon's avatar
Level 51

Unless someone else provides a brilliant idea, I'll just leave my findings here in case it helps someone else in the future.

Turns out the problem is definitely the cookie name. The XSRF-TOKEN cookie does get saved to the proper subdomain. The problem is, with an app on the TLD, there will be multiple XSRF-TOKEN cookies: one for the TLD, and one for each subdomain. When accessing the cookies via javascript, there is no way to differentiate cookies by (sub)domain. So basically the client only sees multiple "XSRF-TOKEN" cookies and if it uses the wrong one, a CSRF mismatch exception will be thrown.

I ended up editing App\Http\Middleware\VerifyCsrfToken.php and overrode a couple methods from the parent class, where the 'XSRF-TOKEN' is hard-coded. I then have the client access the new cookie name and include that in the headers.

VertifyCsrfToken.php

    /**
     * Add the CSRF token to the response cookies.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Symfony\Component\HttpFoundation\Response  $response
     * @return \Symfony\Component\HttpFoundation\Response
     */
    protected function addCookieToResponse($request, $response)
    {
        $config = config('session');

        if ($response instanceof Responsable) {
            $response = $response->toResponse($request);
        }

        $response->headers->setCookie(
			// change hard-coded cookie name to XSRF-TOKEN-PORTAL
            new Cookie(
                'XSRF-TOKEN-PORTAL', $request->session()->token(), $this->availableAt(60 * $config['lifetime']),
                $config['path'], $config['domain'], $config['secure'], false, false, $config['same_site'] ?? null
            )
        );

        return $response;
    }


    /**
     * Get the CSRF token from the request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     */
    protected function getTokenFromRequest($request)
    {
        $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');

		// change hard-coded name to XSRF-TOKEN-PORTAL
        if (! $token && $header = $request->header('X-XSRF-TOKEN-PORTAL')) { 
            try {
                $token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
            } catch (DecryptException $e) {
                $token = '';
            }
        }

        return $token;
    }


And then within Axios, I'll end up doing something like:

axios.interceptors.request.use(function (config) {
  let cookieArray = document.cookie.split(";");

 // this can probably be improved by using a regex.. but this works for now
  for(var i = 0; i < cookieArray.length; i++) {
    let cookiePair = cookieArray[i].split("=");
   
    if(cookiePair[0].trim() == 'XSRF-TOKEN-PORTAL') {
        axios.defaults.headers.common['X-XSRF-TOKEN-PORTAL'] = decodeURIComponent(cookiePair[1]);
    }
}  
  return config;
}, function (error) {
  return Promise.reject(error);
});

I still question if this is the best solution, but it seems to work. Still seems like a lot of effort just to make another Sanctum-powered app work on a sub domain.

3 likes
Mikkol's avatar

@talinon Just a quick question, I'm running into the exact same issue, but in this case in our production environment. It could be me, but the Axios related code doesn't seem to work. Where did you put it in your Vue project?

Thanks in advance!

Talinon's avatar
Level 51

@mikkol I put it within store/auth.js

Looking back on it, looks like below is where I left off with it. I created an environment variable to hold a unique XSRF token name for each app. It hasn't given me any headaches in over a year now.

axios.interceptors.request.use(function (config) {

     let match = document.cookie.match(new RegExp("(^| )" + process.env.VUE_APP_XSRF_TOKEN_NAME + "=([^;]+)"));
     let token = match ? match[2] : '';

    if (token)
        axios.defaults.headers.common['X-' + process.env.VUE_APP_XSRF_TOKEN_NAME] = decodeURIComponent(token);     

    return config;

}, function (error) {
  return Promise.reject(error);
});
2 likes

Please or to participate in this conversation.