Skip to content

Instantly share code, notes, and snippets.

@jaygaha
Created January 12, 2024 02:57
Show Gist options
  • Save jaygaha/e0c65ff39981658c441bd3d0ccbeafd8 to your computer and use it in GitHub Desktop.
Save jaygaha/e0c65ff39981658c441bd3d0ccbeafd8 to your computer and use it in GitHub Desktop.
How to make Laravel Sanctum's XSRF-TOKEN cookie available for each environment [Laravel Sanctum]

While I was working on one of my projects using Laravel Sanctum using SPA Authentication. This project has multiple sub-domains for different environments, like DEV, STG, UAT and is hosted in the same domain, like dev.domain.tld, stg.domain.tld. In the .env file, the SESSION_DOMAIN variable should include a leading dot (.) before the subdomain. This ensures that the session cookie is available to all subdomains. For example:

SESSION_DOMAIN=.domain.tld

Basic authentication flow

  1. Access GET sanctum/csrf-cookie to initialize CSRF protection; it will set the session for the request and XSRF-TOKEN as a cookie in the browser. With this session, it will persist.
  2. After issuing the token, the user can now login to the system.

Problem

I had accessed the DEV environment, and it has generated the cookie in the browser already. And while accessing other environments like STG and UAT, the same cookie (XSRF-TOKEN) is used. It may result in CSRF token mismatch (419) conflicts in the backend, and the user can't use the service.

Solution

After doing a lot of digging on the internet, I found that we need to generate and rename the CSRF token. Till Laravel 10, there is no any straightforward method to rename the default cookie name. So did some overriding the default methods. I have used an environment-based renaming of the token as:

For the DEV environment it was renamed to XSRF-TOKEN-DEV, environment name as appended behind the token name.

Modify the app/Http/Middleware/VerifyCsrfToken.php file by overriding some inbuilt methods:

<?php

namespace App\Http\Middleware;

use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Cookie\CookieValuePrefix;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Cookie;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array<int, string>
     */
    protected $except = [
        //
    ];

    /**
     * Custom CSRF token name
     *
     * @var string
     */
    protected string $cookieCsrf;

    public function __construct(\Illuminate\Contracts\Foundation\Application $app, \Illuminate\Contracts\Encryption\Encrypter $encrypter)
    {
        $this->cookieCsrf = $this->getCustomCookieName();
        parent::__construct($app, $encrypter);
    }

    /**
     * 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);
        }

        $cookieName = $this->cookieCsrf;
        $response->headers->setCookie(
            // change hard-coded cookie name to according to server env
            new Cookie(
                $cookieName, $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)
    {
        $cookieName = $this->cookieCsrf;
        $token = $request->input('_token') ?: $request->cookie($cookieName);

        // change hard-coded name to according to server env
        if (! $token && $header = $request->cookie($cookieName)) {
            try {
                $token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
            } catch (DecryptException $e) {
                $token = '';
            }
        }

        return $token;
    }

    /**
     * Determine if the cookie contents should be serialized.
     *
     * @return bool
     */
    public static function serialized()
    {
        return EncryptCookies::serialized('X-'.self::$cookieCsrf);
    }

    /**
     * Get custom name according to environment server like dev, stg
     *
     * @return string
     */
    protected function getCustomCookieName(): string
    {
        $fullUrl = url()->full();
        $hostUrl = parse_url($fullUrl, PHP_URL_HOST);
        $subDomain = explode('.', $hostUrl)[0];
        $firstPortion = explode('-', $subDomain)[0];
        $cookieCsrf = 'XSRF-TOKEN';

        if (in_array($firstPortion, ['dev', 'stg', 'val'])) {
            $cookieCsrf = $cookieCsrf.'-'.Str::upper($firstPortion);
        }

        return $cookieCsrf;
    }
}
	

After updating the file, clear the cache and config in the server using terminal:

php artisan cache:clear
php artisan config:clear

And also clear the browser cache.

With this changes, I have solved the CSRF Token mismatch (419) issue using the multiple sub-domain. I hope it will also help you to solve this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment