Why axios request /sanctum/csrf-cookie in vuejs to laravel Sanctum returns 204 No Content?

Viewed 4346

I try to use laravel 8 Sanctum in my vuejs 2 Spa app and I created 2 app. But running request from SPA page :

  axios.get('/sanctum/csrf-cookie').then(response => {
    console.log('response::')
    console.log(response)

  })

I got returned:

status: 204, statusText: "No Content"

I followed some articles like : https://blog.codecourse.com/setting-up-laravel-sanctum-airlock-for-spa-authentication-with-vue/

and seems I followed in backend app, as :

In .env I added/modified lines :

SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=localhost

SESSION_DRIVER=cookie

i run client on http://localhost:8080/ host

In /routes/api.php I modified :

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

//Route::middleware('auth:api')->get('/user', function (Request $request) {
//    return $request->user();
//});

In config/cors.php :

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

'supports_credentials' => true,

In config/sanctum.php :

    'stateful' => explode(',', env(
        'SANCTUM_STATEFUL_DOMAINS',
        'localhost:8080,localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1,'.parse_url(env('APP_URL'), PHP_URL_HOST)
    )),
...
    'middleware' => [
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
    ],

I modified app/Http/Kernel.php :

protected $middlewareGroups = [
    'api' => [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],


];

Seacrhing in net for decisons I found several branches with decision,like SPA vue frontend and Laravel 7 backend Sanctum

I tried to modify file app/Providers/RouteServiceProvider.php, but originally it was different as mentioned in the branch above :

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * The path to the "home" route for your application.
     *
     * This is used by Laravel authentication to redirect users after login.
     *
     * @var string
     */
    public const HOME = '/home';

    /**
     * The controller namespace for the application.
     *
     * When present, controller route declarations will automatically be prefixed with this namespace.
     *
     * @var string|null
     */
    // protected $namespace = 'App\\Http\\Controllers';

    /**
     * Define your route model bindings, pattern filters, etc.
     *
     * @return void
     */
    public function boot()
    {
        $this->configureRateLimiting();

        $this->routes(function () {
            Route::prefix('api')
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));

            Route::middleware('web')
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));
        });
    }

    /**
     * Configure the rate limiters for the application.
     *
     * @return void
     */
    protected function configureRateLimiting()
    {
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
        });
    }
}

So it does not have mapApiRoutes method and I tried in 2 way :

    public function boot()
    {
        $this->configureRateLimiting();

        $this->routes(function () {
/*            Route::prefix('api')
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));*/

            Route::prefix('api')
                 ->middleware('web')
                 ->namespace($this->namespace)
                 ->group(base_path('routes/api.php'));

            Route::middleware('web')
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));
        });
    }

// Or added mapApiRoutes function
    protected function mapApiRoutes() 
    {
        Route::prefix('api')
             ->middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }

But It did not help and I still got 204 error.

What is wrong ?

In my composer.json :

"require": {
    "php": "^7.3|^8.0",
    "fideloper/proxy": "^4.4",
    "fruitcake/laravel-cors": "^2.0",
    "guzzlehttp/guzzle": "^7.0.1",
    "laravel/framework": "^8.12",
    "laravel/sanctum": "^2.10",
    "laravel/tinker": "^2.5",
    "laravel/ui": "^2.0"
},

MODIFIED BLOCK : Searching how to fix the issue I payed attention that : 1)I did not find file app/Providers/SanctumServiceProvider.php, even I run command

Project$ php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Copied Directory [/vendor/laravel/sanctum/database/migrations] To [/database/migrations]
Publishing complete.

So I manually copied file from vendor/laravel/sanctum/src/SanctumServiceProvider.php

into app/Providers/SanctumServiceProvider.php and it has content :

<?php

namespace Laravel\Sanctum;

use Illuminate\Auth\RequestGuard;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Http\Controllers\CsrfCookieController;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

class SanctumServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        config([
            'auth.guards.sanctum' => array_merge([
                'driver' => 'sanctum',
                'provider' => null,
            ], config('auth.guards.sanctum', [])),
        ]);

        if (! $this->app->configurationIsCached()) {
            $this->mergeConfigFrom(__DIR__.'/../config/sanctum.php', 'sanctum');
        }
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        if ($this->app->runningInConsole()) {
            $this->registerMigrations();

            $this->publishes([
                __DIR__.'/../database/migrations' => database_path('migrations'),
            ], 'sanctum-migrations');

            $this->publishes([
                __DIR__.'/../config/sanctum.php' => config_path('sanctum.php'),
            ], 'sanctum-config');
        }

        $this->defineRoutes();
        $this->configureGuard();
        $this->configureMiddleware();
    }

    /**
     * Register Sanctum's migration files.
     *
     * @return void
     */
    protected function registerMigrations()
    {
        if (Sanctum::shouldRunMigrations()) {
            return $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
        }
    }

    /**
     * Define the Sanctum routes.
     *
     * @return void
     */
    protected function defineRoutes()
    {
        if ($this->app->routesAreCached() || config('sanctum.routes') === false) {
            return;
        }

        Route::group(['prefix' => config('sanctum.prefix', 'sanctum')], function () {
            Route::get(
                '/csrf-cookie',
                CsrfCookieController::class.'@show'
            )->middleware('web');
        });
    }

    /**
     * Configure the Sanctum authentication guard.
     *
     * @return void
     */
    protected function configureGuard()
    {
        Auth::resolved(function ($auth) {
            $auth->extend('sanctum', function ($app, $name, array $config) use ($auth) {
                return tap($this->createGuard($auth, $config), function ($guard) {
                    app()->refresh('request', $guard, 'setRequest');
                });
            });
        });
    }

    /**
     * Register the guard.
     *
     * @param \Illuminate\Contracts\Auth\Factory  $auth
     * @param array $config
     * @return RequestGuard
     */
    protected function createGuard($auth, $config)
    {
        return new RequestGuard(
            new Guard($auth, config('sanctum.expiration'), $config['provider']),
            $this->app['request'],
            $auth->createUserProvider($config['provider'] ?? null)
        );
    }

    /**
     * Configure the Sanctum middleware and priority.
     *
     * @return void
     */
    protected function configureMiddleware()
    {
        $kernel = $this->app->make(Kernel::class);

        $kernel->prependToMiddlewarePriority(EnsureFrontendRequestsAreStateful::class);
    }
}

I see at method defineRoutes whioch has defined :

    Route::get(
        '/csrf-cookie',
        CsrfCookieController::class.'@show'
    )->middleware('web');

So I try to debug this method with logging, like :

class CsrfCookieController
{
    /**
     * Return an empty response simply to trigger the storage of the CSRF cookie in the browser.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function show(Request $request)
    {
        \Log::info( '-1 show.CsrfCookieController ::' . print_r( -1, true  ) );

        if ($request->expectsJson()) {
            \Log::info( '-2 show.CsrfCookieController ::' . print_r( -2, true  ) );
            return new JsonResponse(null, 204);
        }
        \Log::info( '-3 show.CsrfCookieController ::' . print_r( -3, true  ) );

        return new Response('', 204);
    }

and in file laravel.log I see output of “-1” and “-2” lines.

Also my responce output has lines :

Request URL: http://local-vsanc-backend-api.com/sanctum/csrf-cookie
Request Method: GET
Status Code: 204 No Content
Remote Address: 127.0.0.25:80
Referrer Policy: strict-origin-when-cross-origin

http://local-vsanc-backend-api.com - is host of my backend app which is writen in file /etc/hosts(on my local kubuntu 18) as :

127.0.0.1   localhost
127.0.1.1   AtHome

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
...
127.0.0.25      local-vsanc-backend-api.com

I suppose that my both apps frontend are on the same domain...

Can it be the issue ?

MODIFIED BLOCK # 2: I tried as :

  axios.get('/sanctum/csrf-cookie',  { withCredentials: true }).then(response => {
    console.log('response::')
    console.log(response)

  })

also I tried another similar way I used with tymon/jwt-auth:

  let settingCredentialsConfig = {
    withCredentials:true,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials':true
    }
  }
  console.log('settingCredentialsConfig::')
  console.log(settingCredentialsConfig)
  
  axios.get('/sanctum/csrf-cookie', settingCredentialsConfig).then(response => {
    console.log('response::')
    console.log(response)

  })
}

But in both cases 204 No content respoce. It seems to me that something is wrong on server side...

MODIFIED BLOCK # 3 : If I try to make login request afer I got 204 in responce of /sanctum/csrf-cookie I got next 419 error in client code :

login() {
  axios.get('/sanctum/csrf-cookie').then(response => {
    console.log('response:::')
    console.log(response)
    axios.post('/login', {
      email: this.email,
      password: this.password,
    }).then(response2 => {
      console.log('response2:::')
      console.log(response2);
      ...
    }).catch(error => {
      console.log(error);
    })
  })
}

I suppose because of csrf was not set in my app https://prnt.sc/127b8pl Have I to set it manually somehow or some missing packages? package.json of my app :

{
  "name": "vsanc",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "axios": "^0.21.1",
    "core-js": "^3.6.5",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

Opening one of my Laravel 8 app I see cookies are set : https://prnt.sc/12aarn0

Why I have no csrf applied in cookie ?

Thanks!

1 Answers

"...you should ensure your application's session cookie domain configuration supports any subdomain of your root domain. You may accomplish this by prefixing the domain with a leading . within your application's config/session.php configuration file"

'domain' => '.domain.com',

https://laravel.com/docs/8.x/sanctum#spa-authentication

Related