Infinity loop - Axios Interceptor Refresh token in React app - reactjs

I am using axios interceptor for refresh token when the access token is expired. Here is my code:
import axios from 'axios'
import jwt_decode from "jwt-decode";
import dayjs from 'dayjs'
const baseURL = 'http://127.0.0.1:8000'
let authTokens = localStorage.getItem('authTokens') ? JSON.parse(localStorage.getItem('authTokens')) : null
const axiosInstance = axios.create({
baseURL,
headers:{Authorization: `Bearer ${authTokens?.access}`}
});
axiosInstance.interceptors.request.use(async req => {
if(!authTokens){
authTokens = localStorage.getItem('authTokens') ? JSON.parse(localStorage.getItem('authTokens')) : null
req.headers.Authorization = `Bearer ${authTokens?.access}`
}
const user = jwt_decode(authTokens.access)
const isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1;
if(!isExpired) return req
const response = await axios.post(`${baseURL}/api/token/refresh/`, {
refresh: authTokens.refresh
});
localStorage.setItem('authTokens', JSON.stringify(response.data))
req.headers.Authorization = `Bearer ${response.data.access}`
return req
})
export default axiosInstance;
But I got a problem when calling multiple requests, it goes to an infinite loop. Anyone can help me to fix it. Thanks in advance!

Just use setInterval(() => { your code here }) instead :D
you may see some tutorials like w3schools, etc they always add 0 in the end like : setInterval(() => { do some code }, 0)
but it’s not needed :D because default is 0 :)

I would make the interceptor as response interceptor and not a request interceptor.
axios.interceptors.response.use((response) => {
return response
},
function (error) {
if (error.response.status === 401) {
...fetch token and retry
}

Related

`setRequestHeader` fails to execute with source code as a header value, axios and react

I've encountered a very strange problem, implementing axios interceptors for handling the expired token and refreshing it.
Setting
I'm implementing the JWT authentication with access and refresh tokens.
When the request is being sent to the API route that requires JWT authentication, request interceptor is here to make sure the headers contain an Authorization with Bearer token. The response interceptor checks if the new access token is needed, sends a request to refresh it, and finally updates the axios instance with the new config.
I wrote the code following the Dave Gray's video, but with TypeScript.
Problem
When testing this code, I set the refresh token lifetime to be very long, while setting the access token lifetime to be 5 seconds. After it expires, when the request to the protected route is happening, everything goes according to the plan—the logs from the backend contain two successfully completed requests: (1) to the protected route with 401 response and then (2) the refresh request.
At this point, I see the DOMException in the browser console (Chrome and Safari), which states that setRequestHeader fails to execute because a source code function is not a valid header value. Which, of course, it is not! The piece of code is this.
Code
const axiosPrivate = axios.create({
baseURL: BASE_URL,
headers: { "Content-Type": "application/json" },
withCredentials: true,
});
interface IRequestConfig extends AxiosRequestConfig {
sent?: boolean;
}
const useAxiosPrivate = () => {
const { auth } = useAuth()!;
const refresh = useRefreshToken();
React.useEffect(() => {
const requestInterceptor = axiosPrivate.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = config.headers ?? {};
if (!config.headers["Authorization"]) {
config.headers["Authorization"] = `Bearer ${auth?.token}`;
}
return config;
},
async (error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error);
}
);
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as IRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig?.sent) {
const newAccessToken = await refresh();
prevRequestConfig.sent = true;
prevRequestConfig.headers = prevRequestConfig.headers!;
prevRequestConfig.headers[
"Authorization"
] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequestConfig);
}
return Promise.reject(error);
}
);
return () => {
axiosPrivate.interceptors.request.eject(requestInterceptor);
axiosPrivate.interceptors.response.eject(responseInterceptor);
};
}, [auth, refresh]);
return axiosPrivate;
};
Error
DOMException: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'function (header, parser) {
header = normalizeHeader(header);
if (!header) return undefined;
const key = findKey(this, header);
if (key) {
const value = this[key];
if (!parser) {
return value;
}
if (parser === true) {
return parseTokens(value);
}
if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isFunction(parser)) {
return parser.call(this, value, key);
}
if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isRegExp(parser)) {
return parser.exec(value);
}
throw new TypeError('parser must be boolean|regexp|function');
}
}' is not a valid HTTP header field value.
Research
So far, I've only found one similar issue in the internet, which has links to some others. One of them gives me a hint, that it may be the problem with how axios reads the configuration given to an axios instance.
I'm not sure if the problem is indeed somewhere in axios. I'll be extremely grateful for any useful thoughts on this problem!
I had the same problem, I solved it by manually giving value to axiosPrivate instead of axiosPrivate(prevRequestConfig).
const responseIntercept = axiosPrivate.interceptors.response.use(
response => response,
async (error)=>{
const prevRequest = error?.config;
if (error?.response?.status === 403 && !prevRequest?.sent){
const newAccessToken = await refresh();
// console.log(prevRequest);
return axiosPrivate({
...prevRequest,
headers: {...prevRequest.headers, Authorization: `Bearer ${newAccessToken}`},
sent: true
});
}
return Promise.reject(error);
}
);
Thanks to Daniel Dan's solution I could modify Dave's tutorial code:
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as AxiosRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig.sent) {
prevRequestConfig.sent = true;
const newAccessToken = await refresh();
/* --- The modified line --- */
prevRequestConfig.headers = { ...prevRequestConfig.headers };
/* ------------------------- */
prevRequestConfig.headers[
"Authorization"
] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequestConfig);
}
return Promise.reject(error);
}
);
Just Do This in your response interceptor
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as IRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig?.sent) {
const newAccessToken = await refresh();
prevRequestConfig.sent = true;
prevRequestConfig.headers["Authorization"] = `Bearer ${newAccessToken}`;
return axiosPrivate({
...prevRequestConfig,
...{
headers: prevRequestConfig.headers.toJSON(),
},
});
}
return Promise.reject(error);
}
);
When re-sending the request with updated creds, i.e axiosPrivate(config), the headers property needs to be a plain javascript Object but instead it is converted internally to be an AxiosInstance object.
To fix it, just pass a plain Javascript object to the headers property of your prevRequestConfig object.

React refresh token interceptor

I have a problem with refreshing a token. After I updated the token, a request should be made with my configurations, but this does not happen. The token is updated, but the second time the getIserInfo method is executed, it does not work.
My axios interceptors:
import axios from "axios";
import {getToken, logOut, refreshToken, setUser} from "#/services/auth.service";
const HTTP = axios.create({baseURL: process.env.REACT_APP_API_ENDPOINT});
HTTP.interceptors.request.use(
config => {
const token = getToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
)
HTTP.interceptors.response.use(
response => response,
async error => {
debugger;
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const response = await refreshToken();
const {data} = await response;
setUser(data);
} catch (e) {
logOut();
}
return HTTP(originalRequest);
}
return Promise.reject(error);
}
)
export default HTTP
After executing the token, the getUserInfo method should have worked again
Works after removing headers
delete originalRequest["headers"]
return HTTP(originalRequest);
The problem was in axios 1.0+. After removing and switching to version 0.27, everything worked. It's most likely a bug on their part.

Remix.run, Remix-Auth destroy user session if token validation fails

I am using Remix, along with Remix-Auth and using the Twitch API/OAuth, which requires that I check in with their /validate endpoint every hour docs. I had someone recommend that I use a resource route and POST to that if the validation endpoint returned a status of 401, however, I need as I stated before the request needs to be sent every hour I figured maybe I could use something like React-Query to POST to the resource route every hour.
Just pointing out that I use createCookieSessionStorage with Remix Auth to create the session
Problem
I haven't been able to achieve the actual session being destroyed and a user being re-routed to the login page, I have left what actual code I have currently any help or suggestions to actually achieve the session being destroyed and be re-routed to the login page if the validation fails would be greatly appreciated.
// React Query client side, checks if the users token is still valid
const { error, data } = useQuery("TV-Revalidate", () =>
fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${user?.token}`,
},
}).then((res) => res.json())
);
The above React Query returns this
// My attempt at the resource route
// ~routes/auth/destroy.server.ts
import { ActionFunction, redirect } from "#remix-run/node";
import { destroySession, getSession } from "~/services/session.server";
export const action: ActionFunction = async ({request}) => {
const session = await getSession(request.headers.get("cookie"))
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session)
}
})
}
// Second attempt at resource route
// ~routes/auth/destroy.server.ts
import { ActionFunction, redirect } from "#remix-run/node";
import { destroySession, getSession } from "~/services/session.server";
export const action: ActionFunction = async ({request}) => {
const session = await getSession(request.headers.get("cookie"))
return destroySession(session)
}
I attempted using an if statement to POST to the resource route or else render the page, however, this definitely won't work as React errors out because functions aren't valid as a child and page is blank.
//index.tsx
export default function Index() {
const { user, bits, vali } = useLoaderData();
console.log("loader", vali);
const { error, data } = useQuery("TV-Revalidate", () =>
fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${user?.token}`,
},
}).then((res) => res.json())
);
if (data?.status === 401)
return async () => {
await fetch("~/services/destroy.server", { method: "POST" });
};
else
return ( ... );}
You could use Remix' useFetcher hook.
https://remix.run/docs/en/v1/api/remix#usefetcher
// Resource route
// routes/api/validate
export const loader: LoaderFunction = async ({ request }) => {
const session = await getSession(request);
try {
const { data } = await fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${session.get("token")}`
}
});
return json({
data
}, {
headers: {
"Set-Cookie": await commitSession(session),
}
});
} catch(error) {
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session)
}
});
}
}
And then in your route component something like this:
const fetcher = useFetcher();
useEffect(() => {
if (fetcher.type === 'init') {
fetcher.load('/api/validate');
}
}, [fetcher]);
useEffect(() => {
if(fetcher.data?.someValue {
const timeout = setTimeout(() => fetcher.load('/api/validate'), 1 * 60 * 60 * 1000);
return () => clearTimeout(timeout);
}
},[fetcher.data]);

How to logout automatically when session expires while using createAsyncThunk and axios (withcredential) option using react and redux toolkit?

I am trying to logout the user when the session expires after a certain period of time. I am using redux-toolkit with react for my API calls and, hence, using the createAsyncThunk middleware for doing so.
I have around 60 API calls made in maybe 20 slices throughout my application. Also, there is a async function for logout too that is fired up on the button click. Now the problem that I am facing is that if the session expires, I am not able to logout the user automatically. If I had to give him the message, then I had to take up that message from every api call and make sure that every screen of mine has a logic to notify the Unautherised message.
I did check a method called Polling that calls an API after a certain given time. And I believe that this is not a very efficient way to handle this problem.
**Here is a little code that will help you understand how my API calls are being made in the slices of my application. **
// Here is the custom created api that has axios and withcredentials value
import axios from "axios";
const api = axios.create({
baseURL:
process.env.NODE_ENV === "development" ? process.env.REACT_APP_BASEURL : "",
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
export default api;
// My Logout Function!!
export const logoutUser = createAsyncThunk(
"userSlice/logoutUser",
async (thunkAPI) => {
try {
const response = await api.get("/api/admin/logout");
if (response.status === 200) {
return response.data;
} else {
return thunkAPI.rejectWithValue(response.data);
}
} catch (e) {
return thunkAPI.rejectWithValue(e.response.data);
}
}
);
I want to dispatch this function whenever there is a response status-code is 401 - Unauthorised. But I don't want to keep redundant code for all my other API calls calling this function. If there is a middleware that might help handle this, that would be great, or any solution will be fine.
// Rest of the APIs are called in this way.
..........
export const getStatus = createAsyncThunk(
"orgStat/getStatus",
async (thunkAPI) => {
try {
const response = await api.get("/api/admin/orgstat");
if (response.status === 200) {
return response.data;
} else {
return thunkAPI.rejectWithValue(response.data);
}
} catch (e) {
return thunkAPI.rejectWithValue(e.response.data);
}
}
);
const OrgStatusSlice = createSlice({
name: "orgStat",
initialState,
reducers: {
.......
},
extraReducers: {
[getStatus.pending]: (state) => {
state.isFetching = true;
},
[getStatus.rejected]: (state, { payload }) => {
state.isFetching = false;
state.isError = true;
state.isMessage = payload.message;
},
[getStatus.fulfilled]: (state, { payload }) => {
state.isFetching = false;
state.data = payload.data;
},
},
});
.......
If needed any more clearence please comment I will edit the post with the same.
Thank You!!
import axios from 'axios'
import errorParser from '../services/errorParser'
import toast from 'react-hot-toast'
import {BaseQueryFn} from '#reduxjs/toolkit/query'
import {baseQueryType} from './apiService/types/types'
import store from './store'
import {handleAuth} from './common/commonSlice'
import storageService from '#services/storageService'
// let controller = new AbortController()
export const axiosBaseQuery =
(
{baseUrl}: {baseUrl: string} = {baseUrl: ''}
): BaseQueryFn<baseQueryType, unknown, unknown> =>
async ({url, method, data, csrf, params}) => {
const API = axios.create({
baseURL: baseUrl,
})
API.interceptors.response.use(
(res) => {
if (
res.data?.responseCode === 1023 ||
res.data?.responseCode === 6023
) {
if(res.data?.responseCode === 1023){
console.log('session expired')
store.dispatch(handleSession(false))
return
}
console.log('Lopgged in somewhere else')
store.dispatch(handleSession(false))
storageService.clearStorage()
// store.dispatch(baseSliceWithTags.util.resetApiState())
return
// }, 1000)
}
return res
},
(error) => {
const expectedError =
error.response?.status >= 400 &&
error.response?.status < 500
if (!expectedError) {
if (error?.message !== 'canceled') {
toast.error('An unexpected error occurrred.')
}
}
if (error.response?.status === 401) {
// Storage.clearJWTToken();
// window.location.assign('/')
}
return Promise.reject(error)
}
)
try {
let headers = {}
if (csrf) headers = {...csrf}
const result = await API({
url: url,
method,
data,
headers,
params: params ? params : '',
baseURL: baseUrl,
// signal: controller.signal,
})
return {data: result.data}
} catch (axiosError) {
const err: any = axiosError
return {
error: {
status: errorParser.parseError(err.response?.status),
data: err.response?.data,
},
}
}
}
I am also using RTK with Axios. You can refer to the attached image.

Axios - Refresh token loop

so im rather new to axios and context but I have an Auth context that is provided at App level and this context is used by multiple child components. Within this context I have an axios interceptor that checks requests for 401 (unauthorized) and then calls the refresh token api and replaces the token with a new one. My only concern is that the second time the refresh token API is called it goes into an endless loop of calling the refresh token api? Any ideas what im doing wrong? Any help would be greatly appreciated.
AuthContext.js
axios.interceptors.response.use((response) => {
return response
}, function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && originalRequest.url ===
`${BASE_URI}/Identity/Login`) {
history.push('/login');
return Promise.reject(error);
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const localStorage = JSON.parse(sessionStorage.getItem(AUTH_USER))
const refreshToken = localStorage.refreshToken;
return axios.post(`${BASE_URI}/Identity/Refresh`, null,
{
headers: {
'Refresh-Token': refreshToken
}
})
.then(res => {
if (res.status === 201 || res.status === 200) {
console.log("In refresh request !")
console.log(res)
setSession(null, res.data.token, res.data.refreshToken)
axios.defaults.headers.common['authorization'] = 'Bearer ' + res.data.token;
return axios(originalRequest);
}
}).catch((error) => {
console.log("Inside error refresh")
return Promise.reject(error);
})
}
return Promise.reject(error);
});
I have done something similar to get a refresh token when the token expires and I have encountered the same problem, actually, you are using the same instance of Axios, create another instance
const instance = axios.create();
axios.interceptors.request.use(async (config) => {
if (token && refreshToken) {
const data = JSON.parse(atob(token.split('.')[1]));
const time = Math.floor(new Date().getTime() / 1000);
if (data.exp < time) {
instance.defaults.headers.common["Authorization"] = `Bearer ${refreshToken}`;
const { data } = await instance.get(SERVER.API_ROOT + '/tokens/refresh');
if (data?.AccessToken) localStorage.setItem(config.AUTH_TOKEN, data.AccessToken)
else localStorage.clear();
}
return config;
}
Hope the above example will help you
#J.Naude I have done he similar thing but a generic wrapper around axios which i wrote for one of my project that handles almost all the edge cases
https://gist.github.com/tapandave/01960228516dd852a49c74d16c0fddb1
Hey I know this is an old question, but it seems that your problem was using the same axios instance to request a refresh token, essentially creating a nested refresh-token cycle. What you could do is create a new axios instance (alongside with the initial instance, you would use them both) without an interceptor like this: const noInterceptAxios = axios.create();, and then later use it to send requests where you don't need to check the access token, return noInterceptAxios.post(`/Identity/Refresh).then().catch().

Resources