skip to Main Content

Laravel Sanctum CSRF Token Issue: Token Mismatch When Using Axios and SPA

I am facing a persistent 401 Unauthorized error when trying to authenticate using Laravel Sanctum in my Single Page Application (SPA) with Axios. Here’s a breakdown of my setup and the problem:

Setup:

Backend: Laravel 10 with Sanctum for API authentication.

Frontend: React (running on localhost:3000) with Axios as HTTP client.

Laravel API is running on localhost:8080.

CORS and Sanctum Configuration:
I have configured CORS to allow requests from the frontend domain and added withCredentials: true in Axios. My Laravel config/cors.php looks like this:

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['*'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

In sanctum.php, I set stateful domains:

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000'))

.env Configuration:

APP_URL=http://localhost:8080
SERVER_PORT=8080
FRONTEND_URL=http://localhost:3000
SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=localhost:8080,localhost:3000
SESSION_SECURE_COOKIE=false
SESSION_HTTP_ONLY=true
SESSION_EXPIRE_ON_CLOSE=true
SESSION_DRIVER=cookie
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/

Axios Configuration:
I am using Axios with the following configuration:

const baseURL =
  process.env.NEXT_PUBLIC_BACKEND_API_URL || 'http://localhost:8080';

const servicesApi = axiosLib.create({
  baseURL: baseURL,
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  withCredentials: true,
  withXSRFToken: true,
});

Fragment from my web.php Routes:

Route::prefix('api')->group(function () {
  Route::post('/login', [AuthController::class, 'login']);
  Route::middleware(['auth:sanctum'])->group(function () {
        Route::post('/favorites/toggle', [FavoriteController::class, 'favoritesToggle']);
  });
});

Fragment from my next-auth route.js:

  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },

      async authorize(credentials) {
        try {
          const loginResponse = await servicesApi.post('/api/login', {
            email: credentials.email,
            password: credentials.password,
          })

Problem:

CSRF Cookie Request: When I call await servicesApi.get(‘/sanctum/csrf-cookie’), it succeeds (returns 200).

Login Request: Immediately after, I make a POST request to /api/login with email and password. This request fails with a 401 Unauthorized response.

I can see that the CSRF token (X-XSRF-TOKEN) is being sent in the headers, and the XSRF-TOKEN cookie is also set correctly. However, Laravel still throws a TokenMismatchException.

Debugging Findings:

The Laravel tokensMatch() method in VerifyCsrfToken middleware is comparing the session token ($request->session()->token()) with the decrypted XSRF-TOKEN from the cookie.

The session token and the CSRF token from the header do not match.

It seems that the Sanctum CSRF token is encrypted and thus does not match the session token directly.

Questions:

Why is the CSRF token generated by Sanctum not matching the session token in VerifyCsrfToken?

Is there a way to properly decrypt the XSRF-TOKEN or ensure that the token in the session matches the one provided in the header?

Are there any specific steps I am missing to correctly handle CSRF tokens for a SPA using Sanctum?

Any help or guidance on this issue would be greatly appreciated!

2

Answers


  1. Chosen as BEST ANSWER

    Moving routes from api.php to web.php was special for using csrf checking.

    At first I make sanctum csrf cookie GET call and take returned cookie value in header (this is done by axios) and send to backend.


  2. Tried with [https://stackoverflow.com/questions/70248087/why-am-i-getting-a-csrf-token-mismatch-with-laravel-and-sanctum][previous posted link]:

    Changed variables in my local .env:
    SESSION_DOMAIN=localhost
    SANCTUM_STATEFUL_DOMAINS=localhost

    Moved back routes from web.php to api.php. Activated EnsureFrontendRequestsAreStateful usage with $middleware->statefulApi(); in app.php:

    return Application::configure(basePath: dirname(__DIR__))
        ->withRouting(
            web: __DIR__ . '/../routes/web.php',
            api: __DIR__ . '/../routes/api.php',
            commands: __DIR__ . '/../routes/console.php',
            health: '/up',
        )
        ->withMiddleware(function (Middleware $middleware) {
            $middleware->statefulApi();
    

    When I tried to log in, 401 are returned again:
    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search