How to pause and restart API calls in react-redux app while access token is being refreshed? - reactjs

We have a react-redux app that fetches data using multiple API calls with every page load. The app follows the OAuth2 protocol. It has an access token that expires frequently and a refresh token to be used to get a new access token.
If an API call is made with an expired access token, a 401 error is received with error message "API token is expired." Then we need to get a new token from the auth server.
My problem is this:
When a page loads, say 8 API calls were dispatched. We receive 3 successful 200s but from the 4th response onwards, we receive 401 "API token is expired." At that point, I want to put all API calls that I have already made but didn't receive a response or received 401 error in a queue until I refresh the access token. After the access token is successfully refreshed, I want to re-do the API calls saved in the queue. How can I achieve this?
Looking for this online, I found that redux-saga might work, but didn't see any indication that it can be used for this use case.

I also used to handle this case. This is my solution:
/**
* Connect to API
*/
export const makeRequest = (args) => {
const request = fetch(args)//example
return _retryRequestIfExpired(request, args)
}
/**
* Fake get access token.
*/
const _getNewAccessToken = () => {
return new Promise((resolve, reject) => {
resolve('xyz')
})
}
const _retryRequestIfExpired = (request, args) => {
return request.catch(error => {
if (error === 'abc') {//Any reason you want
return _refreshAccessToken()
.then(newAccessToken => {
const updateArgs = {
...args,
headers: {
'Authorization': newAccessToken
}
}
//Retry with new access token
return makeRequest(updateArgs)
})
}
throw error
})
}
/**
* Important
*/
let _isRefreshingToken = false
let _subscribers = []
const _subscribe = subscriber => {
if (typeof subscriber !== 'function' || _subscribers.indexOf(subscriber) !== -1) {
return false
}
_subscribers = [].concat(_subscribers, [subscriber])
}
const _broadcast = (error = null, data) => {
_isRefreshingToken = false
_subscribers.forEach(subscriber => {
subscriber(error, data)
})
_subscribers = []
}
const _refreshAccessToken = () => {
if (_isRefreshingToken) {//If a request is creating new access token
return new Promise((resolve, reject) => {
const subscriber = (error, accessToken) => {
if (error) {
return reject(error)
}
return resolve(accessToken)
}
_subscribe(subscriber)
})
}
_isRefreshingToken = true
return _getNewAccessToken()
.then(accessToken => {
_broadcast(null, accessToken)
return accessToken
})
.catch(error => {
_broadcast(error)
throw error
})
}
/**
* End Important
*/
In this way, only the first request will actually create a new access token and remaining requests will temporarily be stopped until a new access token is created.

Related

Using a client-side only, readable store `set`, to refresh API access token

I am experimenting with using a readable store as the auth token for a web app.
The set function would fetch an authentication token from an API, and then set a timeout for X seconds to refresh the token before it expires. This would then continuously run every X seconds to set the token.
// auth.ts
export const ssr = false;
type LoginResponse = {
access_token: string;
expires: string;
}
export const authToken: Readable<string> = readable("", (set) => {
const setAuth = async () => {
const auth: LoginResponse = await authenticate();
set(auth.access_token);
authTimer(auth);
};
const authTimer = async (auth: LoginResponse) => {
const timeRemaining = Math.round(((new Date(auth.expires).getTime() - Date.now())) * 0.9);
setTimeout(async () => {
if (auth.access_token === authToken) {
console.log("refreshing auth token");
setAuth();
}
}, timeRemaining);
}
setTimeout(setAuth, 0)
});
The main issue I'm having is that this set function is called from the backend, causing an exception to raise when the authenticate() call fails. It should only be called once the front-end is loaded, as there needs to be client side headers set for the request to succeed.
Is this a generally good approach or is this not what set is intended for?
Also I seem to be unable to make value comparisons with the value of the readable (e.g. auth.access_token === authToken will always return false).
Finally, how can I queue API calls to only execute when the authToken is not an empty string?

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.

Token is Invalid in Reactjs application

I try to get a list from the backend in Reactjs component with JWT token but I get an error message {"status":"Token is Invalid"}, please guide me.
My backend API is working fine and my token is saved in the localstore after login.
my frontend used API code
import {API} from "../config";
/**
* to get about pages
* get a single about page
* update a about page
* delete about page
*/
export const getAboutContent = (token) =>{
return fetch(`${API}/about/?token=${token}`, {
method: "GET",
})
.then(response =>{
return response.json();
})
.catch(err =>{
console.log(err);
});
};
about/index.js
const [allAboutContent, setAllAboutContent] = useState([]);
const loadAllAboutContent = () => {
getAboutContent().then(data => {
if(data.error){
console.log(data.error)
} else{
setAllAboutContent(data.data)
}
});
};
useEffect(() =>{
loadAllAboutContent();
}, [])
Please help.
You are invoking getAboutContent in about/index.js file without JWT and hence it not defined. Just update your code to read JWT from localStorage like below
const loadAllAboutContent = () => {
// Read token from local storage
const token = localStorage.getItem('jwt');
// Pass to getAboutContent
getAboutContent(token).then(data => {
if(data.error){
console.log(data.error)
} else{
setAllAboutContent(data.data)
}
});
};
Also, I see you have stored your token as {token: ''}. Maybe, you can directly save it. Otherwise you have to read it like this JSON.parse(localStorage.getItem('jwt')).token

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

How to make the second request wait for the previous one and use its data?

There are 2 react components, both send requests to the server (with componentDidMount) Each request looks to see if the token is too old, if yes, it updates it with the refreshToken.
Problem - the first request will update the token successfully, will return a new token and a new refreshToken. The second request will be sent to the server with the old refreshToken and the server will generate an error.
The solution is to declare a global variable (window.pending). If it is false - we do not update the token, if the token is updated - assign it a promise, wait for its completion and apply the resulting token.
// From component 1
loadClient = async () => {
const token = await this.props.getToken();
// other code
};
// From component 2
loadProject = async () => {
const token = await this.props.getToken();
// other code
};
// НОС
const tokenProviderHOC = WrappedComponent =>
class extends PureComponent<Props> {
request = null;
componentWillUnmount() {
if (this.request !== null) this.request.abort(); //Cancel server request
}
checkToken = userId => async () => {
const { history } = this.props;
// token, refreshToken, expiresIn from localStorage
const token = methods.getToken("token");
const refreshToken = methods.getRefreshToken("refreshToken");
const expiresIn = methods.getExpiresIn("expiresIn");
if (Date.now() >= expiresIn) {
if (window.pending === false) {
console.log("we are first");
let res;
this.request = this._updateToken({ refreshToken, userId });
window.pending = new Promise(async resolve => {
console.log("request start");
res = await this.request.ready(); //ready - start server request
window.pending = false;
console.log("request end", res ? res.token : false);
return res ? resolve(res.token) : resolve(false);
});
}
console.log("Token is already looking");
return window.pending;
}
return token;
};
render() {
const { userId, history, ...props } = this.props;
return <WrappedComponent {...props} getToken={this.checkToken(userId)} />;
}
};
const mapStateToProps = state => ({
userId: userSelectors.getUserInfo(state)?.id || null,
});
export const tokenProvider = compose(
withRouter,
connect(mapStateToProps),
tokenProviderHOC,
);
Must be displayed in the console:
'we are first'
'request start' or 'Token is already looking'
'request end'
Now the console displays:
'we are first'
'request start'
'Token is already looking'
'request end'
'we are first'
'request start'
'Token is already looking'
tokenProvider.js:121 PUT http://site.loc/user/1/send net::ERR_ABORTED 400 (Bad Request)
// because the token is another
request end false'
It seems that the second request is waiting for the first one, and then again trying to find out the token from the server passing it the old refreshToken. How to solve a problem?
Ignoring the HOC stuff, since it doesn't really matter here, something like this should work. The idea is that you have a singleton of the promise that will eventually resolve to a new token.
Also, you can just as well use a (module-)global variable, no need to stash anything in window unless you especially need to.
tokenIsValid and _updateToken should be more or less self-explanatory, and error handling is elided here.
let fetchTokenPromise = null;
let currentToken = null;
function getToken() {
if (currentToken && tokenIsValid(currentToken)) {
// Current token is valid, return it (as a promise).
return Promise.resolve(currentToken);
}
if (!fetchTokenPromise) {
// We're not fetching a token, start fetching one.
fetchTokenPromise = new Promise(async (resolve, reject) => {
const token = await _updateToken(currentToken);
currentToken = token; // Update saved token.
fetchTokenPromise = null; // Clear fetching state.
resolve(token);
});
}
return fetchTokenPromise;
}
// ...
const token = await getToken(); // this will either get the current token, or start refreshing one

Resources