reactjssessionaxiossession-management

Frequent user logouts in React app due to session handling with Axios interceptors


I'm working on a React frontend application and facing a critical issue where users are frequently being logged out. I believe the problem lies in how I'm handling user sessions, specifically with Axios interceptors for token refreshing.

Background:

Tech Stack:

React for the frontend. Axios for API calls. JWT (JSON Web Tokens) for authentication. Tokens are stored in localStorage.

What I'm Trying to Achieve: Implement a reliable token refresh mechanism using Axios interceptors. Ensure that users maintain their authenticated session without frequent logouts. Handle token expiration gracefully by refreshing tokens when necessary.

Below is the code I'm using:

import { useState, useEffect } from 'react'
import axios from 'axios'

const apiUrl = process.env.NEXT_PUBLIC_MY_API_URL

export function useApi(token) {
  //const [token, setToken] = useLocalStorage('token')

  const api = axios.create({
    baseURL: apiUrl,
    timeout: 100000,// 100 secondi
    withCredentials: true,
    'Access-Control-Allow-Credentials': true
  })
  
  api.interceptors.request.use(
    config => {
      const customizedConfig = config

      if (token) {
        customizedConfig.headers = { ...config.headers, ...{ Authorization: `Bearer ${token}` } }
      } else {
        Promise.reject('Not authorized')
      }

      return customizedConfig
    },
    error => Promise.reject(error)
  )

  api.interceptors.response.use(
    (response) => response,
    (err) => {
      if (err.response && err.response.status === 403) {
        console.log('Fetching refresh_token...');
        const data = {
          refreshToken: localStorage.getItem('token')
        }
  
        return axios.post(`${apiUrl}token/refresh`, data).then((token) => {
            // Retry original request with new token
            console.log('refreh', token)
            const { config } = err;
            localStorage.setItem("token", token.data.jwt);
            config.headers.Authorization = `Bearer ${token.data.jwt}`;
            
            return new Promise((resolve, reject) => {
              axios
                .request(config)
                .then((response) => {
                  resolve(response);
                })
                .catch((error) => {
                  reject(error);
                });
            });
          })
          .catch((error) => {
            console.error(error)
            console.log('403 error.No new token received. Removing token from localStorage.');
            localStorage.removeItem('token');
            console.log(error);  
          });

        // maybe redirect to /login if needed !
      }
      return new Promise((resolve, reject) => {
        reject(err);
      });
    },
  );

  api.interceptors.response.use(
    (response) => response,
    (err) => {
      if (err.response && err.response.status === 401) {
        console.log('Fetching refresh_token...');
        const data = {
          refreshToken: localStorage.getItem('token')
        }
  
        return axios.post(`${apiUrl}token/refresh`, data).then((token) => {
            // Retry original request with new token
            console.log('refreh', token)
            const { config } = err;
            localStorage.setItem("token", token.data.jwt);
            config.headers.Authorization = `Bearer ${token.data.jwt}`;
            
            return new Promise((resolve, reject) => {
              axios
                .request(config)
                .then((response) => {
                  resolve(response);
                })
                .catch((error) => {
                  reject(error);
                });
            });
          })
          .catch((error) => {
            console.log('401 error. No new token received. Removing token from localStorage.');
            localStorage.removeItem('token');
            console.log(error);  
          });

        // maybe redirect to /login if needed !
      }
      return new Promise((resolve, reject) => {
        reject(err);
      });
    },
  );

  return api
}

Solution

  • I use these intercepors in my projects, hope it helps:

    const $api = axios.create({
      withCredentials: true,
      baseURL: import.meta.env.VITE_APP_API_URL
    });
    
    $api.interceptors.request.use((config) => {
      config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
      return config;
    });
    
    $api.interceptors.response.use((config) => {
      return config;
    },async (error) => {
      const originalRequest = error.config;
      if (error.response.status == 401 && error.config && !error.config._isRetry) {
        originalRequest._isRetry = true;
        try {
          const response = await axios.get(`${import.meta.env.VITE_APP_API_URL}/users/refresh`, { withCredentials: true });
          localStorage.setItem('token', response.data.accessToken);
          return $api.request(originalRequest);
        } catch (e) {
          console.log('Unauthorized');
        }
      }
      throw error;
    });
    

    So, also, in App component i use this approach:

    export const App: FC = () => {
      const [ isReady, setIsReady ] = useState(false);
      
      useEffect(() => {
        const setup = async () => {
          const logged = await userService.checkAuth(); // will redirect on login page if not authenticated
          if(logged == true) {
            // actions with logged user...
          }
          setIsReady(true);
        };
    
        setup();
      }, []);
    
      if(!isReady) return <AppLoader />;
    
      return (
          <div className='root'>
            <Layout.App>
              <Sidebar />
              <AppRouter />
            </Layout.App>
          </div>
      );
    };
    

    userService.checkAuth() function code:

    async checkAuth() {
        try {
          const response = await axios.get<UserAfterLoginOrRegistrationResponse>(`${import.meta.env.VITE_APP_API_URL}/api/users/refresh`, { withCredentials: true });
          localStorage.setItem('token', response.data.accessToken);
          this.saveLocalUser(response.data.user);
          return true;
        } catch (e) {
          const error = e as AxiosError<ErrorResponse>;
          if(error.response && error.response.status === 401) {
            window.location.href = `${import.meta.env.VITE_APP_THIS_URL}/login`;
          }
        }
      }