419 Page expired using laravel sanctum

Viewed 1242

I'm still building auth methods to work with a SPA, I have installed laravel sanctum and followed the proper steps and created a demo form on the welcome blade just to test things out, I'm correctly receiving xsrf-cookie using sanctum/csrf-cookie, however the login returns 419 page expired. I have tried clearing cache, route, config

Serving on localhost:8000

.env

SANCTUM_STATEFUL_DOMAINS=localhost:8000
SESSION_DOMAIN=localhost

cors

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

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

];

web routes

Route::post('login', function(){
    echo"a";
});
// Route::post('register', [UserController::class,'register']);

Route::get('/',function(){
    return view('welcome');
});

form

    <form method="post" action="/login">
        <button type="submit">Submit</button>
    </form>

I manually visit localhost:8000/sanctum/csrf-cookie, and I can see cookies for xsrf and session are being set, on submit I still get 419 page expired. (adding @csrf made it work, however, this is just for testing, I'm intending to use a SPA)

1 Answers

Here is how it worked for me:

in kernel.php

  'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:5000,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \Illuminate\Session\Middleware\StartSession::class
        ],

in config/sanctum.php

  <?php

 return [

/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),

/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. If this value is null, personal access tokens do
| not expire. This won't tweak the lifetime of first-party sessions.
|
*/

'expiration' => null,

/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/

'middleware' => [
    'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
    'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],

];

in .env (I use subdomains for both the main API and SPA website)

 SESSION_DRIVER=cookie
 SESSION_LIFETIME=86400
 SESSION_DOMAIN=".myapidomain.com"
 SANCTUM_STATEFUL_DOMAINS=".myapidomain.com"

in cors.php

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

in the user model ex: App\User

   use Laravel\Sanctum\HasApiTokens;

i did't use vuejs on frontend so i used Laravel HTTP Facade, so i created a class for managing the SPA requests for both authenticated requests and non authenticated requests

    <?php

    namespace App\Http\Controllers\Api\Settings;

    use GuzzleHttp\Client;
    use Illuminate\Support\Facades\Http;
    use Illuminate\Support\Facades\Session;

   class Master
   {


public $domainApiLink = "https://subdomain.mydomain/api/";
public $domainWebLink = "https://sbdomain.mydomain.com/";

private $cookies;

public function __construct(){
    $this->generate_cookies();
}

public function generate_cookies(){
    if(empty(Session::get('cookies'))) {
       
            $cookies = Http::acceptJson()->asJson()->get($this->domainWebLink . 'sanctum/csrf-cookie')->throw()->cookies();
            Session::put('cookies', $cookies);
    }
}

public function getRequest($get_link_part, $type = '')
{
    $activeLocale = app()->getLocale();
    if(Session::has('auth_token') ) {
        $response = Http::acceptJson()->asJson()
            ->withToken( Session::get('auth_token'))
            ->withOptions([
                //'debug' => true
            ])
            ->get($this->domainApiLink . $activeLocale . $get_link_part)
            ->throw();
    }else{
          $response = Http::acceptJson()->asJson()
            ->withToken($this->website_token)
            ->withOptions([
                //'debug' => true,
                'cookies' => Session::get('cookies'),
            ])
            ->get($this->domainApiLink . $activeLocale . $get_link_part)
            ->throw();
    }
    switch ($type) {
        case 'json':
            $response = $response->json();
            break;
        case 'assoc':
            if (is_array($response->json())) {
                return $response->json();
            } else {
                $response = json_decode($response->json(), true);
                if (empty($response)) {
                    return ['error' => 'Invalid Json response'];
                }
            }
            break;
        case 'object':
            $response = $response->object();
            break;
        default:
            $response = $response->body();
            break;
    }
    return $response;
}

public function postRequest($inputs, $get_link_part, $type = '')
{
    $activeLocale = app()->getLocale();
    if(Session::has('auth_token') ) {
        $response = Http::acceptJson()->asJson()
            ->withToken(Session::get('auth_token'))
            //->withOptions(['debug' => true])
            ->post($this->domainApiLink . $activeLocale . $get_link_part, $inputs)
            ->throw();
    }
    else{
        $response = Http::acceptJson()->asJson()
            ->withToken($this->website_token)
            ->withOptions([
                //'debug' => true,
            ])
            ->post($this->domainApiLink. $activeLocale . $get_link_part, $inputs)
            ->throw();
    }
    switch ($type) {
        case 'json':
            $result = $response->json();
            break;
        case 'assoc':
            if (is_array($response->json())) {
                $result =  $response->json();
            } else {
                $result = json_decode($response->json(), true);
                if (empty($result)) {
                    $result = ['error' => 'Invalid Json response'];
                }
            }
            break;
        case 'object':
            $result = $response->object();
            break;
        default:
            $result = $response->body();
            break;
    }
    return $result;
}
    /**
    * @return mixed
    */
    public function getCookies()
    {
       return $this->cookies;
    }

    /**
    * @param mixed $cookie
    */
    public function setCookies($cookie): void
    {
        $this->cookies = $cookie;
    }

}

i hope this helps you and i am happy to explain more, if needed

Related