1

I am working on a project with a React front-end and a Laravel back-end. I am trying to set up my authentication system. I am utilizing SPA authentication using Sanctum. I am successfully utilizing the sanctum/csrf-cookie route, where the XSRF-Token cookie is given. When I then try to follow that up with a login, I get a 419 error, CSRF token mismatch. There is no XSRF-Token. What is interesting is that if I do a get request, as in the 'testing' route below, the XSRF cookie is present. However, when I do a post request, as in posting to the login route, the cookie is not present and I get a 419 error.

I am running this locally right now. The front-end is running at localhost:3000, with the back-end running at localhost:8888. Here are various relevant segments of code.

LoginForm.js

let data = {
  email: e.target[0].value,
  password: e.target[1].value
}
axios.get('http://localhost:8888/sanctum/csrf-cookie')
.then((res) => {
  axios.post('http://localhost:8888/login', data)
  .then((res) => {
    axios.get('http://localhost:8888/user')
  })
})

Kernel.php

protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \Fruitcake\Cors\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\HandleInertiaRequests::class,
        ],

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

    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];

.env

SESSION_DRIVER=cookie
CLIENT_URL=http://localhost:3000
SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=http://localhost:3000

Bootstrap.js

axios = require('axios');

axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.withCredentials = true;

Web.php

Route::get('/testing', function () {
    return "Testing.";
});

Route::post('/login', function(Request $request) {
    $credentials = $request->validate([
        'email' => ['required', 'email'],
        'password' => ['required'],
    ]);

    if (Auth::attempt($credentials)) {
        $request->session()->regenerate();
        $id = Auth::id();
        $user = User::find($id);
        return $user;
    }

    return back()->withErrors([
        'email' => 'The provided credentials do not match our records.',
    ]);
});

Sanctum.php

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

Cors.php

'paths' => [
        'api/*',
        'sanctum/csrf-cookie',
        'login',
        'logout',
        'register',
        'user/password',
        'forgot-password',
        'reset-password',
        'user/profile-information',
        'email/verification-notification',
        'testing',
        'user',
        'checkAuth'
    ],

'allowed_methods' => ['*'],

'allowed_origins' => [env('CLIENT_URL')],

'allowed_origins_patterns' => [],

'allowed_headers' => ['*'],

'exposed_headers' => [],

'max_age' => 0,

'supports_credentials' => true,
Clay Raymond
  • 13
  • 1
  • 5
  • `Access-Control-Allow-*` are **response** headers that come from the server. They do not belong in your request and in general will more than likely result in errors. – Phil Nov 07 '22 at 00:57
  • Thank you for that info! I removed that line, and unfortunately am still having the same issue. – Clay Raymond Nov 07 '22 at 01:05

3 Answers3

1

Let's review one by one what is needed to apply SPA authentication using sanctum.

  • First, they must share same top-level domains, however each could be hosted on subdomains, on you case you are using localhost, so this should be fine.
  • Next, you should configure which domains your SPA will be making requests from. SANCTUM_STATEFUL_DOMAINS. If you are accessing your application via a URL that includes a port (127.0.0.1:8000), you should ensure that you include the port number with the domain. Here, it seems you are missing the port.
  • Next, you should add middleware as mentioned in https://laravel.com/docs/9.x/sanctum#sanctum-middleware which seems you have already done it.
  • Next fix cors issue: First, you need to set supports_credentials to be true on config/cors.php. Next, you should enable the withCredentials option on your application's global axios instance. axios.defaults.withCredentials = true;
  • Finally, you should ensure your application's session cookie domain configuration supports any subdomain of your root domain. SESSION_DOMAIN, looks like you have already set localhost here.

Overall, it seems you need to fix to things:

  • you need to add port if you are accessing it from port.
  • you need to set supports_credentials to true on config/cors.php

For more detail, you can follow: https://laravel.com/docs/9.x/sanctum#spa-authentication

Saroj Shrestha
  • 2,696
  • 4
  • 21
  • 45
  • I appreciate the thorough response. I went back and added the port in .env. I also added the sanctum.php portion to the OP, which shows the same thing done in that file. I also added the portion from config/cors.php showing supports_credentials to be true. Unfortunately, I am still having the same error. – Clay Raymond Nov 07 '22 at 04:42
0

As per the Laravel Sanctum documentation :

Additionally, you should ensure that you send the Accept: application/json header with your request.

Since we cannot find misconfigurations in your setup, I will recommend adding a custom middleware in your api group that automatically adds the application/json header :

/* app/Http/Middleware/AjaxHeader.php */

namespace App\Http\Middleware;

use Closure;

class AjaxHeader
{
    /**
     * Handle incoming request
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $request->headers->add([
            'accept' => 'application/json',
        ]);

        return $next($request);
    }
}

And add it to your 'api' middlewareGroups :

/* app/Htpp/Kernel.php */
// ...
protected $middlewareGroups = [
   // ...
   'api' => [         
      \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
      'throttle:api',
      \Illuminate\Routing\Middleware\SubstituteBindings::class,
      \App\Http\Middleware\AjaxHeader::class,
   ],
];
// ...
mlegrix
  • 799
  • 5
  • 12
  • I have implemented these changes, and updated in the OP, but am still getting the same error. – Clay Raymond Nov 07 '22 at 05:47
  • I'm seing that your `web` middleware group doesn't have the ` \Illuminate\Session\Middleware\StartSession::class`, have you removed it? – mlegrix Nov 07 '22 at 06:21
  • I had commented it out, probably because I saw it duplicated from the general $middleware array. But if I re-insert it into the web middleware, I'm still getting the same error. – Clay Raymond Nov 07 '22 at 06:27
  • what does your full cors.php file look like? – mlegrix Nov 07 '22 at 07:05
  • and just making sure, when you modified your configs, have you cleared laravel cache and config ? `php artisan config:clear && php artisan cache:clear` – mlegrix Nov 07 '22 at 07:17
  • After clearing config and cache, I am still getting the same error. I have updated the OP with the full cors.php file. – Clay Raymond Nov 07 '22 at 23:15
  • I've updated the response with another solution, since we cannot fin any mis-configuration in your setup. If it doesn't work, we have to resort using ip address rather than localhost: `localhost:3000 -> 127.0.0.1:3000` – mlegrix Nov 08 '22 at 09:43
-1

I know it's pretty old, but the only solution I found (posted a separate question here about this) is that for POST / PUT etc.. request, I had to get a new token through sanctum (get(sanctum/csrf-cookie)) and then it works. No idea why GET doesn't require it and why POST /PUT / PATCH etc... does.

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 18 '23 at 12:02
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/34850752) – Ram Chander Aug 18 '23 at 12:33