Logic recommendation for React refreshing token - reactjs

Can anyone suggest me a good logic to automatically refresh my accessToken?
At the moment, I have an OpenAPI generated class, where the accessToken is a promise in all the requests. In this promise, I check if the token is expired and I fetch the new one, based on a refresh token.
I also have an AuthContext, that I use to manage my authtentication, user details, etc. (saving to localstorage, etc)
The problem is that I need somehow to access my AuthContext and to give it the new token or to logout if the token could not be refreshed, in my API class.
I do receive an error on the following code, but this is expected:
Invalid hook call. Hooks can only be called inside of the body of a function component.
My access token as promise (inside the API class)
accessToken: new Promise<string>(async (resolve, reject) => {
const tokenExpiresAt = Date.parse(model.tokenExpiration);
const {saveAuth, logout} = useAuth() // Here is the problem
// if token is expired, refresh it
if (tokenExpiresAt < Date.now()) {
this.Auth.refresh({accessToken: model.token, refreshToken: model.refreshToken})
.then((response) => {
// save new auth
saveAuth(response.data)
resolve(response.data.token);
})
.catch((error) => {
// error refreshing token, logout
logout()
reject("Token expired");
});
}
resolve(model.token);
})

I faced a similar problem a while ago, yes you cannot call a hook inside a hook so another approach to solve this issue is to intercept the axios response interceptor.
axiosApiInstance.interceptors.response.use((response) => {
return response
}, async function (error) {
const originalRequest = error.config;
if (error.response.status === 403 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refreshToken');
const access_token = await refreshAccessToken(refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + access_token;
return axiosApiInstance(originalRequest);
}
return Promise.reject(error);
});
And you can implement your own refreshAccessToken function according to your usecase.

Related

how to cancel web requests using interceptors?

I have a code where I try to make a real world simulation. for this example I want to simulate that I want to make a web request if and only if there is a token in localstorage with the key "token". The problem is that it executes the amount of web requests that I have running at the moment.
const getData = async () => {
const data = await instance.get("todos/1");
setData(data.data);
await instance.get("todos/2");
await instance.get("todos/3");
};
.
.
.
instance.interceptors.request.use(async (req) => {
token = localStorage.getItem("token") || null;
console.log(req.url);
if (!token) {
console.log("not exist token");
//cancel request because token not exists
return req;
} else {
console.log("token exist");
req.headers.Authorization = `Bearer ${token}`;
return req;
}
});
My idea is to cancel or not execute the web requests when there is no token in localstorage, and if there is, I would like to send the token in the headers.
How can I do it?
this is my live code
There's a api for request cancellation in axios
https://github.com/axios/axios#cancellation
const cancelSource = axios.CancelToken.source();
const instance = axios.create({
cancelToken: cancelSource.token,
timeout: 1000 * 10,
});
instance.interceptors.request.use((req) => {
if (condition) {
cancelSource.cancel('No Authorization Token')
}
return req
})
BUT, There's much simpler way to abort request.
Just throw an error in the interceptor.
instance.interceptors.request.use((req) => {
if (condition) {
throw new Error('No Authorization Token')
}
return req
})
Either of two ways throws Promise Error.
I recommend the latter one, Throwing error.

Dynamically changing content-type in axios reactjs

So here is my issue.
I am using JWT authentication in my project and i have an axiosInstance setup in my react project . I also have an interceptor for axiosInstance which takes care of intercepting and refreshing tokens when required.
const axiosInstance = axios.create({
​baseURL: baseURL,
​timeout: 360000,
​transformRequest: [
​function (data, headers) {
​const accessToken = window.localStorage.getItem('access_token');
​if (accessToken) {
​headers['Authorization'] = `Bearer ${accessToken}`;
​} else {
​delete headers.Authorization;
​}
​return JSON.stringify(data);
​},
​],
​headers: {
​'Content-Type': 'application/json',
​accept: 'application/json',
​},
});
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
console.log(
'Caught the error response. Here is your request ',
originalRequest,
);
// case 1: No error specified Most likely to be server error
if (typeof error.response === 'undefined') {
// Uncomment this later
alert('Server error occured');
return Promise.reject(error);
}
// case 2: Tried to refresh the token but it is expired. So ask user to login again
if (
error.response.status === 401 &&
originalRequest.url === baseURL + 'auth/api/token/refresh/'
) {
store.dispatch(setLoginFalse());
return Promise.reject(error);
}
// Case 3: Got 401 Unauthorized error. There are different possiblities
console.log('Error message in axios = ', error.response.data);
if (
error.response.status === 401 &&
error.response.statusText === 'Unauthorized'
) {
const refreshToken = localStorage.getItem('refresh_token');
console.log('Refresh token = ', refreshToken);
// See if refresh token exists
// Some times undefined gets written in place of refresh token.
// To avoid that we check if refreshToken !== "undefined". This bug is still unknown need to do more research on this
if (refreshToken !== undefined && refreshToken !== 'undefined') {
console.log(typeof refreshToken == 'undefined');
console.log('Refresh token is present = ', refreshToken);
const tokenParts = JSON.parse(atob(refreshToken.split('.')[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
console.log(tokenParts.exp);
// Case 3.a Refresh token is present and it is not expired - use it to get new access token
if (tokenParts.exp > now) {
return axiosInstance
.post('auth/api/token/refresh/', { refresh: refreshToken })
.then((response) => {
localStorage.setItem('access_token', response.data.access);
axiosInstance.defaults.headers['Authorization'] =
'Bearer ' + response.data.access;
originalRequest.headers['Authorization'] =
'Bearer ' + response.data.access;
console.log('access token updated');
// After refreshing the token request again user's previous url
// which was blocked due to unauthorized error
// I am not sure by default axios performs get request
// But since we are passing the entire config of previous request
// It seems to perform same request method as previous
return axiosInstance(originalRequest);
})
.catch((err) => {
// If any error occurs at this point we cannot guess what it is
// So just console log it
console.log(err);
});
} else {
// Refresh token is expired ask user to login again.
console.log('Refresh token is expired', tokenParts.exp, now);
store.dispatch(setLoginFalse());
}
} else {
// refresh token is not present in local storage so ask user to login again
console.log('Refresh token not available.');
store.dispatch(setLoginFalse());
}
}
// specific error handling done elsewhere
return Promise.reject(error);
},
);
export default axiosInstance;
Note that i have Content-Type set as 'application/json' in axiosIntance.
But my problem is inorder to upload images the content type should be 'multipart/form-data --boundary: set-automatically'.
(NOTE: Manually setting boundary for multipart data doesn't seem to work)
The boundary for multipart data is set automatically by axios if we don't put the content-type in header. But for that i have to somehow delete the content-type from axiosInstance at one place (from where i am uploading the image) without disturbing axiosInstance used at other parts of the project.
I tested it with fetch and by setting up new axios instance it works as expected. But the problem is these requests won't be intercepted by axios for refreshing JWT tokens if needed to.
I read various posts on this, but i still don't see a way to solve this.
I cann provide any more details if required. Please help me, i already spent 8+ hours debugging this.
Thank you.
Edit 1
I changed the handleSubmit function to this
const handleSubmit = (e) => {
e.preventDefault();
console.log(file);
let formData = new FormData();
formData.append('profile_pic', file);
formData.append('name', 'root');
axiosInstance.defaults.headers.common['Content-Type'] =
'multipart/form-data';
axiosInstance
.put('/users/profile-pic-upload/', formData)
.then((res) => console.log(res))
.catch((err) => console.log(err));
};
But the content type is still application/json
But Let's say i changed the content-type in core axios.js to 'multipart/form-data' it changes the content type of all requests. It will break other things but as expected it won't fix this issue. Because setting manual boundary doesn't seems to work. Even this post says to remove the content type during multipart data so that it is handled automatically by library (axios in this case)
For passing anything dynamic to your axios instance, use a function that returns the axios instance like this:
import axios from 'axios';
const customAxios = (contentType) => {
// axios instance for making requests
const axiosInstance = axios.create({
// your other properties for axios instance
headers: {
'Content-Type': contentType,
},
});
// your response interceptor
axiosInstance.interceptors.response.use(// handle response);
return axiosInstance;
};
export default customAxios;
And now, you can use axios like:
import customAxios from './customAxios';
const axiosForJSON = customAxios('application/json');
const axiosForMultipart = customAxios('multipart/form-data');
axiosForJSON.get('/hello');
axiosForMultipart.post('/hello', {});
// OR
cusomAxios('application/json').get('/hello');
axiosInstance.defaults.headers.put['Content-Type'] = "multipart/form-data";
Or
axiosInstance.interceptors.request.use(config => {
config.headers.put['Content-Type'] = 'multipart/form-data';
return config;
});
Try this for your specific instance.
Answer above from Lovlesh Pokra helped me.
In my case of checking the access token when downloading a file - response needs to be parsed for the new access token. However since this interceptor is within the class that is used to download the file with the responseType set to arrayBuffer while creation
responseType: 'arraybuffer'
I had to change the responseType to json like below
youraxiosinstance.defaults.responseType = "json";
and then setting it back to arraybuffer - so file download can continue
youraxiosinstance.defaults.responseType = "arraybuffer";
based upon your need - just before the call - the change can be done as required by you.

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().

Axios request interceptor not working on browser refresh

I am using axios interceptor in my react app to pass the token for each request.
I initially call the setupAxiosInterceptors method after I login (See code below). This works perfectly fine until I refresh the browser.
const registerSucessfulLoginForJwt = (username, token) => {
sessionStorage.setItem(USER_NAME_SESSION_ATTRIBUTE_NAME, username)
setupAxiosInterceptors(createJwtAuth(token)) //Calling the axios interceptor at the time of login
}
See below the setupAxiosInterceptors method
const setupAxiosInterceptors = (token) => {
Axios.interceptors.request.use((config) => {
if(isUserLoggedIn()) {
config.headers.authorization = token
sessionStorage.setItem('authorization', token)
}
return config
})
}
Any thought on how to fix this so it works at all time?
I was able to find a solution to my problem. I create an ApiSetup.js file where I create a custom axios instance which could use for all requests.
const request = axios.create({
baseURL: API_PATH_BASE
})
request.interceptors.request.use(config => {
const currentUser = AuthenticationService.getLoggedInUser() //You can get the user directly from the cookie or session storage...
if(currentUser.userName) {
config.headers.Authorization = currentUser.userToken
}
return config
}, err => {
console.log(err)
return Promise.reject(err)
})
export default request

React-Redux best practice to change auth state

Probably it's simple, but i'm thinking the best way how to update state when user session (in backend) has expired.
The user story - User authenticates against backend service. Session token is saved using localStorage and loaded up every time we need authenticated request - so far everything is good.
Then (let's say after 1h) token has expired. How can i dispatch action and update state?
The code fragments:
export function getPersons() {
return (dispatch) => {
dispatch(fetchDataRequest());
doFetch('persons', {}, true).then((payload) => {
dispatch(receiveData(payload.data));
}).catch((error) => {
if (error.response && error.response.status===401) {
dispatch(logoutUser());
} else {
dispatch(fetchDataError(error.message));
}
})
}
}
As you can see i'm checking the response status here
if (error.response && error.response.status===401) {
dispatch(logoutUser());
}
And i'm using a top level function doFetch with code below
let API_URL = "http://example.com/api/";
export default function doFetch(endpoint, configuration, authenticated) {
let token = localStorage.getItem('token');
let config = {'Content-Type': 'application/json'};
if (authenticated) {
if (token) {
config.headers = { 'Authorization': `${token}` };
} else {
return Promise.reject("No token available");
}
}
Object.assign(config, configuration);
return fetch(API_URL + endpoint, config)
.then(checkStatus)
}
And i want to dispatch the logoutUser action here in top level function to avoid of boilerplate in my code. Is it possible at all?
Or i have to return and check the status wherever i'm making request.
Thanks guys.

Resources