React auth with react-query and axios

React auth with react-query and axios

Posted

I am going to try and show you a simple way to handle login and refresh-token state if you have react-query and axios in your arsenal. This approach relies on that your backend returns the refresh-token in a same-site http-only cookie when your users are logging into your application.

We will utilize the axios-hooks package to achieve this, our initial component will look something like this.

function AuthProvider(props: any) {
		const accessTokenRef = React.useRef<string>();
		const [tokenExpires, setTokenExpires] = React.useState<string>();

		...
}

This is how we handle our critical state, now we need to make the user able to login, that can look like this:

const loginRequest = async (username: string, password: string) => {
  const { token, ...otherDataYouMightNeed } = await yourBackendLoginRequest(
    username,
    password,
  );
  return { token, otherDataYouMightNeed };
};

const loginQuery = useMutation(loginRequest, {
  onSuccess: (data) => {
    // here we rely on the returned data contains the user, the token and its expiration date.
    accessTokenRef.current = data.token;
    setTokenExpires(data.tokenExpires);
  },
});

const refreshRequest = async () => {
  // it is important that you use axios when fetching the refresh-token, that way we know the cookie
  // with the refresh-token is included
  const { token, ...otherDataYouMightNeed } = await axios.get(
    "/your-refresh-token-endpoint",
  );
  return { token, otherDataYouMightNeed };
};

// this request should not have to include any logic as we are sending the token value with the cookeis.
const refreshQuery = useMutation(refreshRequest, {
  onSuccess: (data) => {
    // the refresh-token request should return similiar data as the loginRequest.
    accessTokenRef.current = data.token;
    setTokenExpires(data.tokenExpires);
  },
  // here we set a refetch-interval to avoid us sending a request without a valid access token.
  // you can either hardcode this value or calculate the diff until your token expires.
  refetchInterval: 300000,
});

const login = async (username: string, password: string) => {
  await loginQuery.mutateAsync(username, password);
  // you might want to wrap this in try / catch to handle errors and alert the user
  // if the username/password is incorrect.
};

Now we want to bind the access token to our axios config, submitting the credentials with every request to the backend.

useEffect(() => {
  // add authorization token to each request
  axios.interceptors.request.use(
    (config: AxiosRequestConfig): AxiosRequestConfig => {
      config.baseURL = BASE_URL; // base url for your api.
      config.headers.authorization = `Bearer ${accessTokenRef.current}`;
      // withCredentials should be enabled to submit the cookies with each request.
      // this is essential to refresh the access-token.
      config.withCredentials = true;
      return config;
    },
  );

  axios.interceptors.response.use(
    (response) => response,
    async (error) => {
      return Promise.reject(error);
    },
  );

  // configure axios-hooks to use this instance of axios
  configure({ axios });
}, []);

Now, wherever else you import axios around in the application, the config defined above will be used.

The entire component can look something like this:

const loginRequest = async (username: string, password: string) => {
  const { token, ...otherDataYouMightNeed } = await yourBackendLoginRequest(
    username,
    password
  );
  return { token, otherDataYouMightNeed };
};

const refreshRequest = async () => {
  // it is important that you use axios when fetching the refresh-token, that way we know the cookie
  // with the refresh-token is included
  const { token, ...otherDataYouMightNeed } = await axios.get(
    "/your-refresh-token-endpoint"
  );
  return { token, otherDataYouMightNeed };
};

function AuthProvider(props: any) {
  const accessTokenRef = React.useRef<string>();
  const [tokenExpires, setTokenExpires] = React.useState<string>();

  const loginQuery = useMutation(loginRequest, {
    onSuccess: (data) => {
      // here we rely on the returned data contains the user, the token and its expiration date.
      accessTokenRef.current = data.token;
      setTokenExpires(data.tokenExpires);
    },
  });

  // this request should not have to include any logic as we are sending the token value with the cookies.
  const refreshQuery = useMutation(refreshRequest, {
    onSuccess: (data) => {
      // the refresh-token request should return similiar data as the loginRequest.
      accessTokenRef.current = data.token;
      setTokenExpires(data.tokenExpires);
    },
    // here we set a refetch-interval to avoid us sending a request without a valid access token.
    // you can either hardcode this value or calculate the diff until your token expires.
    refetchInterval: 300000,
  });

  const login = async (username: string, password: string) => {
    await loginQuery.mutateAsync(username, password);
    // you might want to wrap this in try / catch to handle errors and alert the user
    // if the username/password is incorrect.
  };

  useEffect(() => {
    // add authorization token to each request
    axios.interceptors.request.use(
      (config: AxiosRequestConfig): AxiosRequestConfig => {
        config.baseURL = BASE_URL;
        config.headers.authorization = `Bearer ${accessTokenRef.current}`;
        // this is important to include the cookies when we are sending the requests to the backend.
        config.withCredentials = true;
        return config;
      }
    );

    axios.interceptors.response.use(
      (response) => response,
      async (error) => {
        return Promise.reject(error);
      }
    );

    // configure axios-hooks to use this instance of axios
    configure({ axios });
  }, []);

  const isSuccess = loginQuery.isSuccess || refetchQuery.isSuccess;
  const isAuthenticated = isSuccess && !!accessTokenRef.current;
  // if you need a user object you can do something like this.
  const user = refetchQuery.data.user || loginQuery.data.user;

  // example on provider
  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        user,
        login,
        logout,
      }}
      {...props}
    ></AuthContext.Provider>
  );
}

Now you can create a hook where you can use the auth data.

const useAuth = () => {
  const context = React.useContext(AuthContext);
  if (context === undefined) {
    throw new Error("AuthContext must be within AuthProvider");
  }

  return context;
};

Then you can do something like this around in your application (remember to include AppContext.Provider somewhere in your app).

const { isAuthenitcated, user } = useAuth();

And there you have it, a very simple approach to handle authentication and requests with react-query and axios.

Thanks for reading.