skip to Main Content

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 = [
        AppHttpMiddlewareTrustProxies::class,
        FruitcakeCorsHandleCors::class,
        AppHttpMiddlewarePreventRequestsDuringMaintenance::class,
        IlluminateFoundationHttpMiddlewareValidatePostSize::class,
        AppHttpMiddlewareTrimStrings::class,
        IlluminateFoundationHttpMiddlewareConvertEmptyStringsToNull::class,
        IlluminateSessionMiddlewareStartSession::class,
        IlluminateSessionMiddlewareAuthenticateSession::class,
        IlluminateViewMiddlewareShareErrorsFromSession::class,
    ];

    protected $middlewareGroups = [
        'web' => [
            AppHttpMiddlewareEncryptCookies::class,
            IlluminateCookieMiddlewareAddQueuedCookiesToResponse::class,
            IlluminateViewMiddlewareShareErrorsFromSession::class,
            AppHttpMiddlewareVerifyCsrfToken::class,
            IlluminateRoutingMiddlewareSubstituteBindings::class,
            AppHttpMiddlewareHandleInertiaRequests::class,
        ],

        'api' => [
            LaravelSanctumHttpMiddlewareEnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            IlluminateRoutingMiddlewareSubstituteBindings::class,
        ],
    ];

    protected $routeMiddleware = [
        'auth' => AppHttpMiddlewareAuthenticate::class,
        'auth.basic' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class,
        'cache.headers' => IlluminateHttpMiddlewareSetCacheHeaders::class,
        'can' => IlluminateAuthMiddlewareAuthorize::class,
        'guest' => AppHttpMiddlewareRedirectIfAuthenticated::class,
        'password.confirm' => IlluminateAuthMiddlewareRequirePassword::class,
        'signed' => IlluminateRoutingMiddlewareValidateSignature::class,
        'throttle' => IlluminateRoutingMiddlewareThrottleRequests::class,
        'verified' => IlluminateAuthMiddlewareEnsureEmailIsVerified::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,

2

Answers


  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

    Login or Signup to reply.
  2. 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 AppHttpMiddleware;
    
    use Closure;
    
    class AjaxHeader
    {
        /**
         * Handle incoming request
         *
         * @param IlluminateHttpRequest $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' => [         
          LaravelSanctumHttpMiddlewareEnsureFrontendRequestsAreStateful::class,
          'throttle:api',
          IlluminateRoutingMiddlewareSubstituteBindings::class,
          AppHttpMiddlewareAjaxHeader::class,
       ],
    ];
    // ...
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search