skip to Main Content

I’m implementing a Laravel API + React SPA with Sanctum authentication.

With Sanctum, before requesting the actual login route, you need to send a request to /sanctum/csrf-cookie ‘to initialize csrf protection’.

Currently I have this RTK Query api:

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { API_HOST } from "./config";

export const authApi = createApi({
  reducerPath: "authApi",
  baseQuery: fetchBaseQuery({
    baseUrl: `${API_HOST}`,
  }),
  endpoints: (builder) => ({
    initCsrf: builder.mutation<void, void>({
      query() {
        return {
          url: "sanctum/csrf-cookie",
          credentials: "include",
          headers: {
            "X-Requested-With": "XMLHttpRequest",
            "Content-Type": "application/json",
            Accept: "application/json",
          },
        };
      },
    }),
    loginUser: builder.mutation<{ access_token: string; status: string }, { username: string; password: string }>({
      query(data) {
        return {
          url: "login",
          method: "POST",
          body: data,
          credentials: "include",
          headers: {
            "X-Requested-With": "XMLHttpRequest",
            "Content-Type": "application/json",
            Accept: "application/json",
          },
        };
      },
      async onQueryStarted(args, { dispatch, queryFulfilled }) {
        try {
          await queryFulfilled;
        } catch (err) {
          console.error(err);
        }
      },
    }),
    logoutUser: builder.mutation<void, void>({
      query() {
        return {
          url: "logout",
          credentials: "include",
        };
      },
    }),
  }),
});

export const { useLoginUserMutation, useLogoutUserMutation, useInitCsrfMutation } = authApi;

Then in my login page, when the user clicks the login button, I call:

const onSubmitHandler: SubmitHandler<LoginInput> = (values) => {
    initCsrf()
      .then(() => {
        loginUser(values);
      })
      .catch((err) => {
        console.error(err);
      });
  };

The first request is working and sets the cookie but the second returns with a 419 CSRF Token mismatch exception.

Examining the requests, the login request contains the XSRF-TOKEN cookie with the token that I got in the first request so it should work ok.

This worked before with Axios using the same structure (first request to establish the cookie and the second including the cookie).

2

Answers


  1. Chosen as BEST ANSWER

    Turns out that axios decodes the xsrf-token and rtk query does not. The token includes an = sign at the end which is encoded to %3D.

    I just had to decode the token with decodeURIComponent after reading it from the document and it did the trick.

    Source: https://github.com/laravel/framework/discussions/42729#discussioncomment-2939906


  2. I got stuck on this for quite a while so I thought I would share what I did more explicitly.

    RTK Query with credentials: "include" set will set the cookie on the document of the page. We can gather this cookie in the transformResponse of initCSRF and put it in our state. Then we can use prepareHeaders to grab our state and set the X-XSRF-TOKEN for all our api responses.

    const getCookie = (cookieName: string):string|undefined => {
      const cookieArray = document.cookie.split(';');
    
      for (const cookie of cookieArray) {
        let cookieString = cookie;
    
        while (cookieString.charAt(0)==' ') {
          cookieString = cookieString.substring(1,cookieString.length);
        }
        if (cookieString.indexOf(cookieName + '=') == 0) {
          return cookieString.substring(cookieName.length + 1,cookieString.length);
        }
    
      }
    
      return undefined;
    }
    
    export const api = createApi({
      baseQuery: fetchBaseQuery({
        baseUrl: 'http://localhost/',
        prepareHeaders: (headers: Headers, api): Headers => {
          const token = (api.getState() as RootState).auth.csrfToken;
          if (typeof token == 'string') {
            headers.set('X-XSRF-TOKEN', token);
          }
    
          return headers;
        }
      }),
      endpoints: (builder) => ({
        initCsrf: builder.mutation<string, void>({
          query() {
            return {
              url: "sanctum/csrf-cookie",
              credentials: "include",
              headers: {
                "X-Requested-With": "XMLHttpRequest",
                "Content-Type": "application/json",
                Accept: "application/json",
              },
            };
          },
          transformResponse: (apiResponse, meta, arg):string => {
            let cookie = getCookie('XSRF-TOKEN');
            if (typeof cookie != 'undefined') return decodeURIComponent(cookie);
    
            return '';
          },
        }),
        loginUser: builder.mutation<{ access_token: string; status: string }, LoginRequest>({
          query(data) {
            return {
              url: "api/login",
              method: "POST",
              body: data,
              credentials: "include",
              headers: {
                "X-Requested-With": "XMLHttpRequest",
                "Content-Type": "application/json",
                Accept: "application/json",
              },
            };
          },
        }),
      }),
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search