axios interceptor not resending failed request - reactjs

I am creating this React dashboard app. I'm currently trying to set up an axios interceptor to refresh my access token when I send a request with an expired access token to my backend.
I have a refresh async action in my slice that makes a get request to my backend with my refresh token cookie. That is being called from my axiosInstance response interceptor.
After that returns, I set the access token in my store
Then try to rerun the original failed request
return axiosInstance(originalRequest)
The problem is the return statement to rerun the request is not working. when I view my devtools network tab I only see the preflight, failed post request, then refresh request.
There should be another retry of the first post request after the refresh
Here is my axios code
-Creating my instance and interceptors
import axios from 'axios';
import { store } from '../../../app/store';
import { refresh } from '../../auth/authSlice';
const axiosInstance = axios.create({
timeout:5000
})
axiosInstance.defaults.baseURL = process.env.REACT_APP_BASE_SCRAPE_URL
axiosInstance.defaults.withCredentials = true;
axiosInstance.interceptors.request.use(function(config){
const state = store.getState();
const token = state.auth.accessToken
config.headers.Authorization = `Bearer ${token}`
return config
})
axiosInstance.interceptors.response.use(resp => resp,async error =>{
const orginalRequest = error.config
console.log(error.response);
if(error.response.data === "Invalid Access Token"){
store.dispatch(refresh({}))
return axiosInstance(orginalRequest)
}
})
export default axiosInstance
Using the axios Instance (Request that needs to rerun after initial failed request)
import axiosInstance from "./protectedScrapeAxios"
export const start = async (pageName, postCount) =>{
const resp = await axiosInstance.post('/start', {pageName: pageName, postCount: postCount})
return resp.data
}

Related

React Logout User and Redirect to Login Page when Refresh Token Expires

I am having this challenge in React JS. I have designed my system to use Token Refresh and Token Rotation, so when the token expires, the backend deletes the cookie automatically, which should also happen in the frontend by deleting the localStorage variable that stores the token and redirecting user to the login page. I am using Axios interceptors to automatically check on response errors if it is error 403 and hit on the /refresh endpoint with the refresh token. The challenge is, when this refresh fails, meaning the token has expired, I am unable to redirect the user automatically to the login page. That is, the localStorage token is not deleted which should happen when the refresh token fails. It takes 2 or 3-page refreshes for the token to be deleted and the user to be finally redirected to the login page. During these attempts, no data is loaded, which is expected since the backend has already logged out the user by deleting the cookie from the backend, hence it can be frustrating to the users. This is my code for further understanding.
axiosPrivate.js
import { setIsAuthenticated } from '../features/auth/authSlice';
import instance from "./axiosConfig";
import { memoizedRefreshToken } from "./axiosRefreshToken";
instance.interceptors.request.use(
async (config) => {
const authenticatedUser = JSON.parse(localStorage.getItem("authenticatedUser"));
if (authenticatedUser?.accessToken) {
config.headers = {
...config.headers,
authorization: `Bearer ${authenticatedUser?.accessToken}`,
};
}
return config;
},
(error) => Promise.reject(error)
);
instance.interceptors.response.use(
(response) => response,
async (error) => {
const config = error?.config;
if (error?.response?.status === 403 && !config?.sent) {
config.sent = true;
console.log("Inside If: ", config);
const result = await memoizedRefreshToken();
if (result?.accessToken) {
console.log("Access Token Returned: ", result)
config.headers = {
...config.headers,
authorization: `Bearer ${result?.accessToken}`,
};
} else {
console.log("No Access Token ")
store.dispatch(setIsAuthenticated(false));
}
return instance(config);
}
console.log("Outside If: ", config);
store.dispatch(setIsAuthenticated(false));
return Promise.reject(error);
}
);
export const axiosPrivate = instance;
axiosRefreshToken.js
import { store } from '../features/store';
import { setIsAuthenticated } from '../features/auth/authSlice';
import instance from "./axiosConfig";
const refreshTokenFn = async () => {
try {
const response = await instance.get("/auth/refresh");
const authenticatedUser = response.data;
if (!authenticatedUser?.accessToken) {
localStorage.removeItem("authenticatedUser");
store.dispatch(setIsAuthenticated(false));
}
localStorage.setItem("authenticatedUser", JSON.stringify(authenticatedUser));
store.dispatch(setIsAuthenticated(true));
return authenticatedUser;
} catch (error) {
localStorage.removeItem("authenticatedUser");
store.dispatch(setIsAuthenticated(false));
}
};
const maxAge = 10000;
export const memoizedRefreshToken = mem(refreshTokenFn, {
maxAge,
});
I have a feeling that the problem is in the axiosRefreshToken.js but I am unable to trace down what I am doing wrong. Kindly advise.
UPDATE
I am thinking that the issue is in the axiosRefreshToken.js where when there is no response, nothing is returned and the error catching after that does not as well work as expected. My expectation is that when there is no response, error catching under that kicks in and deletes the localStorage token immediately. But by debugging, it will take like 3 page refreshes to get that error catching working.
After so much digging and testing, I think the issue with the code was using memoization of the function in the axiosRefreshToken.js file. I removed the mem function and it automatically catches the error and fires the dispatch function instantly when the token is not refreshed. However, if there is a better way of handling this, I would gladly welcome it.

NextAuth: How can I attach my JWT token to every axios call?

I am building a Next.js application.
For authentication, I am using NextAuth.
For making HTTP calls, I am using Axios.
The problem I am facing is that I want to attach the JWT with every axios call I make.
I have an axios instance created as:
axios-client.js
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
throw new Error(error.response.data.message);
},
);
return instance;
};
export default ApiClient();
I can get the jwt from getSession() function provided by next-auth.
But the problem is that function is asynchronous. If I try to get jwt from this from the getSession() function, I always get a "Promise" instead of value.
PS: I am using Strapi which sends the JWT after successful login.
Why can't you await the response from getSession() and add it as a header to your request. Note that you don't have to reinitialize axios client each time, you can reuse it.

Why doesn't this update of an axios interceptor work?

I have an axios client, defined in a provider context and accessed throughout my app with a useAxios hook.
The provider uses an access token, which gets updated every few minutes using a token refresh. This is definitely updating correctly.
I use an interceptor to add the token to the request headers. But of course, I want the latest token. So, each time the token updates, I change the interceptor to use the new one. I do this in a useEffect which only fires if the token changes, and I have verified that this effect is firing correctly.
But, any use of the axios client subsequent to a token refresh still uses the original interceptors - the ones that were defined when the app was first loaded. Consequently, after the original token times out, all my requests are unauthenticated even though I've added an interceptor with an up to date token, because teh original interceptor is being used...
THE QUESTION
How do I correctly update this interceptor so the latest one is used on every query?
THE CODE
import React, { useEffect } from 'react'
import axios from 'axios'
import { useAuth } from './useAuth'
import { apiErrors } from 'errors'
export const AxiosContext = React.createContext(undefined)
const AxiosProvider = ({ children }) => {
const { accessToken } = useAuth()
const axiosClient = React.useMemo(() => {
return axios.create({
headers: {
'Content-Type': 'application/json',
},
})
}, [])
// Attach request interceptor to add the auth headers
useEffect(() => {
console.log(
'VERIFIED TO WORK - THIS SHOWS PERIODICALLY ON TOKEN UPDATE, WITH THE NEW TOKEN',
accessToken
)
axiosClient.interceptors.request.use((request) => {
if (accessToken) {
request.headers.Authorization = `JWT ${accessToken}`
}
return request
})
}, [axiosClient, accessToken])
return (
<AxiosContext.Provider value={axiosClient}>
{children}
</AxiosContext.Provider>
)
}
// A hook for accessing this authenticated axios instance
function useAxios() {
return React.useContext(AxiosContext)
}
export { AxiosProvider, useAxios }

How can I access redux store outside a component?

I've a API file and a token in redux store. I've tried with getState() but the token will stored after a user signs in so it's giving me error of undefined. How do I solve this problem?
const token = store.getState().verification.token;
import axios from "axios";
const ApiInstance = axios.create({
});
// console.log(process.env);
ApiInstance.defaults.headers.post["content-type"] = "application/json";
ApiInstance.defaults.headers.get["Accept"] = "application/json";
ApiInstance.defaults.headers.get["Authorization"] =
`Bearer ${token}`;
export default ApiInstance;
Use Axios Interceptor! :)
You shouldn't use .defaults for authorization in most cases anyway because ideally your token will expire after some time period. A better approach would be to use an axios interceptor.
Bascially axios lets you define a function that will run EVERY time an HTTP request is made. In that function you can call store.getState() and then append the token to the request headers.
For example, you could use the following:
ApiInstance.interceptors.request.use(req => {
let myToken = store.getState().verification.token;
req.headers.authorization = `Bearer ${myToken}`;
return req;
});

How to modify axios instance after exported it in ReactJS?

I am using:
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
to set header after user make login in application, but when refresh the page this configuration is removed.
I would like to set this configuration for all requests from axios, when user make login.
I got do that setting this configuration manually, putting this line of code before to export axios instance.
Now, I need to set this configuration when user make login. How can I do that?
You're probably going to want to write a middleware module to get/set the token in localStorage and apply it to your Axios instance. In the past when I used Axios, I typically did it like this:
import axios from 'axios';
import { API_URL } from '../constants/api';
const API = axios.create({
baseURL: `${API_URL}`,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
API.interceptors.request.use(
config => {
const token = sessionStorage.getItem('jwt');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
delete API.defaults.headers.common.Authorization;
}
return config;
},
error => Promise.reject(error)
);
export default API;
You'll need to create functions to get/set the JWT in localStorage, but if you do that, this should work for you. This will fetch the JWT from localStorage before making each request, so it won't break even if the page is refreshed as long as the user has a valid JWT in localStorage.
I have the same issue as Hiatt described:
refresh the page will invalidate my previous default config
while I don't feel like reading storage before every request
so what I did is check before request and read if necessary (eg: default were reset due to page reload
// request interceptor function
function check_before_request(config) {
if (! config.headers.common['Authorization']) {
const token = Cookies.get('Authorization')
if (! token){
removeCookies()
location.href = `/login?redirect=${encodeURIComponent(location.pathname)}`
}
else {
setHeaderAuth(token, config)
}
return config
}
else return config
}
// also can be used in login page but without the second param
function setHeaderAuth(token, config) {
Cookies.set('Authorization', token)
axios.defaults.headers.common['Authorization'] = token;
if (config){
config.headers['Authorization'] = token
}
}

Resources