mstdmstd's avatar

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

Hello, 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 https://stackoverflow.com/questions/62733796/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"
    },

Thanks!

0 likes
7 replies
mstdmstd's avatar

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 ?

wingly's avatar

Not sure what you are trying to fix here. You are supposed to receive a 204 no content response this is correct. This is just to trigger the browser to store the CSRF cookie.

mstdmstd's avatar

Is it mentioned somehwere in docs https://laravel.com/docs/8.x/sanctum ? 204 response seems no sense as I need to get code and keep it on client side ? What I see in my browser : prnt.sc/126mtsm and details prnt.sc/126n0p5: Is it correct response ?

wingly's avatar

@mstdmstd from the docs:

To authenticate your SPA, your SPA's "login" page should first make a request to the /sanctum/csrf-cookie endpoint to initialize CSRF protection for the application:

axios.get('/sanctum/csrf-cookie').then(response => { // Login... }); During this request, Laravel will set an XSRF-TOKEN cookie containing the current CSRF token. This token should then be passed in an X-XSRF-TOKEN header on subsequent requests, which some HTTP client libraries like Axios and the Angular HttpClient will do automatically for you. If your JavaScript HTTP library does not set the value for you, you will need to manually set the X-XSRF-TOKEN header to match the value of the XSRF-TOKEN cookie that is set by this route.

And if you take a look at the source it self

    /**
     * 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)
    {
        if ($request->expectsJson()) {
            return new JsonResponse(null, 204);
        }

        return new Response('', 204);
    }

So basically since you are using axios you wont have todo anything more the header should be attached to all your subsequent requests. Maybe you need to configure axios to have withCredentials option set to true but at least from what you are saying that's not your problem

mstdmstd's avatar

I added option

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

After I got 204 code I see Set-Cookie Responce headers. But checking cookies of my browsers I see it empty : https://prnt.sc/127b8pl In settings of my browser : https://prnt.sc/127bbgs also if this way is good if it is dependable on cookies set on clients browser ?

mstdmstd's avatar

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 ?

Please or to participate in this conversation.