Cannot authenticate using spa

290 Views Asked by At

I have my API that run on this url: https://example.com, and my vue.js app which runs on this: https://app.example.com. So I configured as the following the .env:

SESSION_DOMAIN=.example.com
SANCTUM_STATEFUL_DOMAINS=https://app.example.com

the axios configuration contains the following:

import axios from 'axios'

const server = axios.create({
  baseURL: 'https://example.com'
})
server.defaults.withCredentials = true
server.defaults.withXSRFToken = true

within the file sanctum.php I setted as guard api:

'guard' => ['api']

and within kernel.php I did:

'api' => [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

I also added:

\Illuminate\Session\Middleware\StartSession::class,

within $middleware group.

The application flow is the following: when I press the login button I sent a request to:

/sanctum/csrf-cookie

which return 204 and set the cookie, then I perform the login request that return 204, so far everything seems good. At the end, I try to sent a GET request to /user that is protected as the following:

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', [UserController::class, 'index']);
});

but I get unauthorized. I spend the whole day try to figur out, tried a lot of solutions but no one has worked.

I get the same result on postman.

UPDATE

config/sanctum.php

<?php

use Laravel\Sanctum\Sanctum;

return [

    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
        Sanctum::currentApplicationUrlWithPort()
    ))),

    'guard' => ['web'],

    'expiration' => null,

    'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),

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

];

config.cors.php

<?php

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

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

];

config/session.php

<?php

use Illuminate\Support\Str;

return [

    'driver' => env('SESSION_DRIVER', 'file'),

    'lifetime' => env('SESSION_LIFETIME', 120),

    'expire_on_close' => false,

    'encrypt' => false,

    'files' => storage_path('framework/sessions'),

    'connection' => env('SESSION_CONNECTION'),

    'table' => 'sessions',

    'store' => env('SESSION_STORE'),

    'lottery' => [2, 100],

    'cookie' => env(
        'SESSION_COOKIE',
        Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
    ),

    'path' => '/',

    'domain' => env('SESSION_DOMAIN'),

    'secure' => env('SESSION_SECURE_COOKIE'),

    'http_only' => true,

    'same_site' => 'lax',

    'partitioned' => false,

];

config/auth.php

<?php

return [

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],


    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => 10800,

];

.env

SESSION_DRIVER=cookie
SESSION_LIFETIME=120

SESSION_DOMAIN=.example.com
SANCTUM_STATEFUL_DOMAINS=https://app.example.com

Notice that both domains are https registered with letsencrypt, I just hided them for privacy reason.

4

There are 4 best solutions below

7
Mohammad Al-Ani On

I think you missed setting the supports_credentials option to true in config/cors.php

1
Karan Datwani On

I can feel your pain, I struggled with it at my time. I'm using the same practice for my web app. I achieved it with the same steps.

I notice one difference in env. I use:

SESSION_DOMAIN=.get-set-sold.test
SANCTUM_STATEFUL_DOMAINS=.get-set-sold.test

☝️ Use "." and remove https://

1
suxgri On

try hitting the following controller when logging in (i assumed you log in with email and password):

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

class LoginController extends Controller
{
  public function login()
  {
    request()->validate([
        'email' => ['required', 'string', 'email'],
        'password' => ['required'],
    ]);

    /**
     * We are authenticating a request from our frontend.
     */
    if (EnsureFrontendRequestsAreStateful::fromFrontend(request())) {
        $this->authenticateFrontend();
    }
    /**
     * We are authenticating a request from a 3rd party.
     */
    else {
        // Use token authentication.
    }
    //here you could check that session contains authentication 'entry'
}

private function authenticateFrontend()
{
    if (! Auth::guard('web')
        ->attempt(
            request()->only('email', 'password'),
            request()->boolean('remember')
        )) {
        throw ValidationException::withMessages([
            'email' => __('auth.failed'),
        ]);
    }
}
}

Check the response in devtools also.

1
Gary Archer On

This seems to be a cookie not sticking problem. To troubleshoot an SPA and API setup you will need to trace browser HTTP requests and look at request / response headers. Here are a couple of pointers:

CORS

When using cross origin cookies and returning the Access-Control-Allow-Credentials header you cannot set allowed origins to *. You need to use an exact domain name:

'allowed_origins' => ['https://app.example.com']

COOKIE SETTINGS

Use these settings if you want to send the cookie in both API requests and static content requests, and so that you use cookies with the best security settings:

Domain: .example.com
SameSite: strict

LOCAL PC SETUP

Reproduce problems on your local computer by editing your /etc/hosts file and defining development domains:

127.0.0.1 example-dev.com app.example-dev.com

Then run your SPA using a URL such as http://app example-dev.com:3000 and the API at a URL such as http://example-dev.com:3001`.

The SPA and API must run in the same site as represented by these domain names. If this is not true the browser will consider cookies to be third-party and is likely to drop them.

Same site cookie logic can use different ports and must use the same scheme (http or https). Your setup is same site and cross origin.

NEXT STEPS

See if this configuration helps. If you still experience problems, update your question with HTTP request details:

  • Pre-flight OPTIONS request and response headers
  • The response headers that set the set-cookie header
  • The request headers for the following request, which should contain a cookie header