Infinite request loop while trying to refresh access token - javascript

I'm trying to refresh jwt access token by using axios interceptor and refresh token.
The access token is refreshed and replaced properly in the local storage after a reissue request,
but reissue request does not stop. And when there is an error, it falls into a infinite loop which has already happened several times and caused shutdown...
I've tried to force the page to reload after getting the response thinking it is because access token is not saved properly in the local storage but i think it is not the case.
useAxios is a reissue custom hook that is used in every token required requests.
how do i stop this infinite loop?
const useAxios = () => {
const accessToken = localStorage.getItem("accessToken");
const refreshToken = getCookie("refreshToken");
const email = localStorage.getItem("email");
if(accessToken) {
axiosInstance.interceptors.request.use(async (req) =\> {
console.log(jwtDecode(accessToken).exp);
const expired = Date.now() >= jwtDecode(accessToken).exp * 1000;
if (!expired) return req;
try {
const res = await axios.get(
`${process.env.REACT_APP_BASE_URL}api/auth/re-issue/${email}`,
{
headers: {
Refresh: refreshToken,
},
},
{ withCredentials: true }
);
// console.log(res);
localStorage.setItem("accessToken", res.headers.get("Authorization"));
window.location.reload();
} catch (e) {
console.log(e);
}
return req;
});
}
return axiosInstance;
};
export default useAxios;

Related

JWT auth process - frontend part

Im making a user authorization process with JWT tokens.
How does the flow look like?
User logs in - gets an access token and a refresh token from a server, as a response
Access token comes in json body and is saved in local storage. Refresh token comes in a httpOnly cookie.
User can use getAllUsers method untill access token is valid.
Whenever getAllUsers method returns 401 unauthorized (when access token expires), there is a request being sent to refresh token endpoint - getRefreshToken, which returns new access token that is being saved to local storage
Refresh token expires and user is being logged out.
Whole flow in Postman works but i have got problem at frontend side.
Function getAllUsers works until access token expires.
Thats why I made a global function in a util file that checks if a response is 401 and if so, it sends a request to get a new access token and calls a function which returned that error.
However it does not work.
I think that the problem is in getAllUsers function which immediately goes to catch block (when cant fetch list of users because of 401) and does not invoke that global function from util file. Console logs from both functions (getDataFromResponse, getRefreshToken) does not work so it does not even get there.
Any ideas??
API utils file
import { AxiosResponse } from "axios";
import { apiService } from "./api.service";
type ApiServiceMethods = keyof typeof apiService;
export const getDataFromResponse = async (
response: AxiosResponse,
funName: ApiServiceMethods,
...args: any
): Promise<any> => {
if (response.status === 401) {
console.log("error");
await apiService.getRefreshToken();
return await apiService[funName](args);
}
return response.data;
};
API Service:
import { getDataFromResponse } from "./api.utils";
import axios from "./axios";
type LoginArgs = {
password: string;
username: string;
};
const apiServiceDef = () => {
const login = async (args: LoginArgs) => {
try {
const response = await axios.post("/login", {
username: args.username,
password: args.password,
});
const { data } = response;
const { token } = data;
localStorage.setItem("accessToken", token);
return response;
} catch (e) {
throw new Error("Custom");
}
};
/* problem here */
const getAllUsers = async () => {
const Token = localStorage.getItem("accessToken");
try {
const response = await axios.get("/users", {
headers: {
Token,
},
});
return await getDataFromResponse(response, "getAllUsers");
} catch (e) {
console.log(e);
}
};
/* problem here */
const getRefreshToken = async () => {
try {
console.log("fetch new access token");
const response = await axios.get("/refreshToken");
if (response.status === 401) {
localStorage.removeItem("accessToken");
throw new Error("TokenExpiredError");
}
const { data } = response;
const { token } = data
localStorage.setItem("accessToken", token);
return response;
} catch (e) {
console.log(e);
}
};
return { login, getRefreshToken, getAllUsers };
};
export const apiService = apiServiceDef();
I usually use a wrapper around the async functions or just use axios interceptors (https://stackoverflow.com/a/47216863/11787903). Be sure that err.response.status is right property, not sure about that, but this solution should work for you.
const asyncWrapper = async (handler) => {
try {
return handler()
} catch (err) {
if (err.response.status === 401) {
// refresh token then again call handler
await refreshToken()
return handler()
}
}
}
const getAllUsers = asyncWrapper(() => {
const Token = localStorage.getItem("accessToken");
return axios.get("/users", {
headers: {
Token,
},
});
});

Next js data fetching with axios instance and Authorization

I'm using NextJS 12.0.10 with next-redux-wrapper 7.0.5
And Axios custom instance to hold user JWT token saved in local storage and inject it with every request also to interceptors incoming error's in each response
The problem with this is that I simply cannot use the Axios instance inside the Next data fetching methods
Because there is no way to bring user JWT Token from local storage when invoking the request inside the server
Also, I cannot track the request in case of failure and send the refresh token quickly
I tried to use cookies but getStaticProps don't provide the req or resp obj
Should I use getServerSideProps always
axios.js
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 20000,
headers: {
common: {
Authorization: !isServer()
? localStorage.getItem("access_token")
? "JWT " + localStorage.getItem("access_token")
: null
: null,
accept: "application/json",
},
},
});
login-slice.js
export const getCurrentUser = createAsyncThunk(
"auth/getCurrentUser",
async (_, thunkApi) => {
try {
const response = await axiosInstance.get("api/auth/user/");
await thunkApi.dispatch(setCurrentUser(response.data));
return response.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
toast.error(error.message);
return thunkApi.rejectWithValue(error.message);
}
}
);
Page.jsx
export const getStaticProps = wrapper.getStaticProps((store) => async (ctx) => {
try {
await store.dispatch(getCurrentUser());
} catch (e) {
console.log("here", e);
}
return {
props: {},
};
});
Server side rendered technology is a one-way street if you follow the standard practise. You won't get any local details - being it cookies, local store or local states back to the server.
I would let the server build the DOM as much as it makes sense (ie with empty user data) and let the client fetch the data via useEffect.

Getting error on console despite handling it (MERN + Redux)

I am sending axios get request whose end-point sends the user associated with the token stored in localStorage and then the redux state is updated with the user. When I don't have a token the end-point return a res with status 401 with message "Unauthorized" and then I handle it in the catch statement and set the "error" redux state. But even after doing this the error is displayed on the console like this:
Failed to load resource: the server responded with a status of 401 (Unauthorized) /users/auth:1
This is the function which makes api call and authorizes the user:
export function loadUser(){
return function (dispatch,getState){
dispatch(userLoading());
const token = getState().auth.token;
const config = {
headers:{
'Content-Type':'application/json'
}
}
if(token) config.headers['auth-token']=token;
axios.get('http://localhost:80/users/auth',config)
.then(user => {
dispatch(clearError())
dispatch(userLoaded(user.data))
})
.catch(error => {
dispatch(setError(error.response.status,error.response.data.msg));
dispatch(authError());
})
}
}
This is the middleware which handles the token before hitting the endpoint (In my case response is returned from here itself since there is no token sent):
function auth(req,res,next){
const token = req.header('auth-token');
if(!token) res.status(401).json({msg:"Unauthorized"})
else{
try{
const decoded = jwt.verify(token,jwt_secret);
req.user = decoded;
next();
}
catch(e){
res.status(400).json({msg:"Invalid token"})
}
}
}
I'm not able to figure out why am I getting error on console (State is getting updated as desired)
It is actually impossible to do with JavaScript. because of security concerns and a potential for a script to hide its activity from the user.
The best you can do is clearing them from your console.
console.clear();
I think it is because you are not getting the token when consulting your API.
If this is the case I recommend you use defaults.headers.common in this way
const axiosApi = axios.create({ baseURL: "http://localhost:80" });
const headerAuth = () => {
const token = getMyToken();
if (token) {
axiosApi.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} else {
delete axiosApi.defaults.headers.common.Authorization;
}
};
export function loadUser(){
headerAuth(); // <-----
return function (dispatch,getState){
dispatch(userLoading());
axiosApi.get('/users/auth',config)
.then(user => {
dispatch(clearError())
dispatch(userLoaded(user.data))
})
.catch(error => {
dispatch(setError(error.response.status,error.response.data.msg));
dispatch(authError());
})
}
I recommend that you do not store the token in the REDUX but in sessionStorage

How to integrate getAccessToken with fetch function to load data from DRF backend to React Frontend?

React newbie here, but proficient in Django.I have a simple fetch function which worked perfectly but then my project had no login authentication involved. Now that I have configured the login system, my backend refuses to serve requests with any access tokens. My login authentication is very new to me and was more or less copied from somewhere. I am trying to understand it but am not able to. I just need to know how to convert my simple fetch function to include the getAccessToken along the request in it's headers so my backend serves that request.
Here is my previously working simple fetch function :
class all_orders extends Component {
state = {
todos: []
};
async componentDidMount() {
try {
const res = await fetch('http://127.0.0.1:8000/api/allorders/'); // fetching the data from api, before the page loaded
const todos = await res.json();
console.log(todos);
this.setState({
todos
});
} catch (e) {
console.log(e);
}
}
My new login JWT authentication system works perfectly, but my previous code is not working and I keep getting error
"detail": "Authentication credentials were not provided."
This is is the accesstoken I am not able to 'combine' with my preivous fetch function:
const getAccessToken = () => {
return new Promise(async (resolve, reject) => {
const data = reactLocalStorage.getObject(API_TOKENS);
if (!data)
return resolve('No User found');
let access_token = '';
const expires = new Date(data.expires * 1000);
const currentTime = new Date();
if (expires > currentTime) {
access_token = data.tokens.access;
} else {
try {
const new_token = await loadOpenUrl(REFRESH_ACCESS_TOKEN, {
method: 'post',
data: {
refresh: data.tokens.refresh,
}
});
access_token = new_token.access;
const expires = new_token.expires;
reactLocalStorage.setObject(API_TOKENS, {
tokens: {
...data.tokens,
access: access_token
},
expires: expires
});
} catch (e) {
try {
if (e.data.code === "token_not_valid")
signINAgainNotification();
else
errorGettingUserInfoNotification();
} catch (e) {
// pass
}
return reject('Error refreshing token', e);
}
}
return resolve(access_token);
});
};
If you're looking for a way how to pass headers in fetch request, it's pretty straight forward:
await fetch('http://127.0.0.1:8000/api/allorders/', {
headers: {
// your headers there as pair key-value, matching what your API is expecting, for example:
'details': getAccessToken()
}
})
Just don't forget to import your getAccessToken const, if that's put it another file, and I believe that would be it. Some reading on Fetch method

Wrap javascript fetch to add custom functionality

I would like to know if it is possible to do this, because I'm not sure if I'm wrong or if it isn't possible. Basically, what I want to do is to create a wrap function for native fetch javascript function. This wrap function would implement token validation process, requesting a new accessToken if the one given is expired and requesting again the desired resource. This is what I've reached until now:
customFetch.js
// 'url' and 'options' parameters are used strictely as you would use them in fetch. 'authOptions' are used to configure the call to refresh the access token
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
// Actual server request that user wants to do.
const request = window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken();
}
else {
return d.json();
}
});
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = () => {
window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
// For this example, we can omit this, we can suppose we always receive the access token
if (d.status === 401) {
// Unauthorized and the cookie used to validate and refresh the access token has expired. So we want to login in to the app again
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
return window.customFetch(url, options, authOptions);
}
else {
console.log('no token has been sent');
return null;
}
});
}
return request;
}
consumer.js
const getResourcePrivate = () => {
const url = MAIN_URL + '/resource';
customFetch(url, {
method: 'get'
},{
url: AUTH_SERVER_TOKEN,
unauthorizedRedirect: AUTH_URI,
tokenName: TOKEN_NAME
}).then((json) => {
const resource = json ? json.resource : null;
if (resource) {
console.log(resource);
}
else {
console.log('No resource has been provided.');
}
});
}
I'll try to explain a little better the above code: I want to make transparent for users the token validation, in order to let them just worry about to request the resource they want. This approach is working fine when the accessToken is still valid, because the return request instruction is giving to the consumer the promise of the fetch request.
Of course, when the accessToken has expired and we request a new one to auth server, this is not working. The token is refreshed and the private resource is requested, but the consumer.js doesn't see it.
For this last scenario, is it possible to modify the flow of the program, in order to refresh the accessToken and perform the server call to get the private resource again? The consumer shouldn't realize about this process; in both cases (accessToken is valid and accessToken has expired and has been refreshed) the consumer.js should get the private requested resource in its then function.
Well, finally I've reached a solution. I've tried to resolve it using a Promise and it has work. Here is the approach for customFetch.js file:
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
const requestResource = (resolve) => {
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken(resolve);
}
else {
resolve(d.json());
}
});
}
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = (resolve) => {
window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
if (d.status === 401) {
// Unauthorized
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
requestResource(resolve);
}
else {
console.log('no token has been sent');
return null;
}
});
}
let promise = new Promise((resolve, reject) => {
requestResource(resolve);
});
return promise;
}
Basically, I've created a Promise and I've called inside it to the function which calls to server to get the resource. I've modified a little the request(now called requestResource) and refreshAccessToken in order to make them parametrizable functions. And I've passed to them the resolve function in order to "resolve" any function once I've received the new token.
Probably the solution can be improved and optimized, but as first approach, it is working as I expected, so I think it's a valid solution.
EDIT: As #Dennis has suggested me, I made a mistake in my initial approach. I just had to return the promise inside the refreshAccessToken function, and it would worked fine. This is how the customFetch.js file should look (which is more similar to the code I first posted. In fact, I've just added a return instruction inside the function, although removing the start and end brackets would work too):
// 'url' and 'options' parameters are used strictely as you would use them in fetch. 'authOptions' are used to configure the call to refresh the access token
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
// Actual server request that user wants to do.
const request = window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken();
}
else {
return d.json();
}
});
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = () => {
return window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
// For this example, we can omit this, we can suppose we always receive the access token
if (d.status === 401) {
// Unauthorized and the cookie used to validate and refresh the access token has expired. So we want to login in to the app again
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
return window.customFetch(url, options, authOptions);
}
else {
console.log('no token has been sent');
return null;
}
});
}
return request;
}

Categories