response of axios retry in main.js in vue js - vue.js

I have a method named getUsers and it is in created hook in Users Component and I have access token and refresh token in my local storage.
I want that when my token expires, I use refresh token and get new access token and retry last request that was failed because of expired access token.
My problem is I want get response of second try of axios call in first axios call point (in Users component in created hook) because I fill table from response of it.
How can I do that?
main.js:
axios.interceptors.request.use((config) => {
config.headers['Content-Type'] = `application/json`;
config.headers['Accept'] = `application/json`;
config.headers['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`;
return config;
}, (err) => {
return Promise.reject(err);
});
let getRefreshError = false
axios.interceptors.response.use((response) => {
return response
},
(error) => {
const originalRequest = error.config;
if (!getRefreshError && error.response.status === 401) {
axios.post(process.env.VUE_APP_BASE_URL + process.env.VUE_APP_REFRESH_TOKEN,
{refresh_token: localStorage.getItem("refresh_token")})
.then(res => {
localStorage.setItem("access_token", res.data.result.access_token);
localStorage.setItem("refresh_token", res.data.result.refresh_token);
originalRequest.headers['Authorization'] = localStorage.getItem("access_token");
return axios(originalRequest)
.then((res) => {
return Promise.resolve(res);
}, (err) => {
return Promise.reject(err);
});
}).catch(error => {
getRefreshError = true;
router.push('/pages/login')
return Promise.reject(error);
})
}
return Promise.reject(error);
});
Users:
created() {
this.getUsers();
}

You can return a new Promise from error handler of response interceptor. Refresh token there, perform the original request and resolve promise based on the result of actions (refreshing and re-fetching). Here is a general sketch of what you should do.
axios.interceptors.response.use(
(res => res),
(err => {
return new Promise(resolve, reject) => {
// refresh token
// then save the token
// then reperform original request
// and resolve with the response of the original request.
resolve(resOfSecondRequest)
// in case of any error, reject with the error
// and catch it where original call was performed just like the normal flow
reject(errOfSecondRequest)
}
})
)

Related

react admin and AuthProvider no redirect after login

I've notice (using some console.log) that the method checkAuth is called some times only before the login methods and not after, so
when the login is accomplished correctly, the token is stored in the browser's local storage and at the end the method login returns the resolved promise,
the checkAuth is not anymore invoked and the page redirect is not performed by the dashboard
If then i change manually the page, it works correctly because the token is in the localstorage and the checkAuth method is able to check it normally
This is my AuthProvider
import axios from "axios";
export default {
// called when the user attempts to log in
login: ({ username, password }) => {
username = encodeURIComponent(username);
password = encodeURIComponent(password);
const tokenUrl = "https://myendpoint/profili/token";
const headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
const config = {
headers
};
const data = `username=${username}&password=${password}&grant_type=password`;
axios.post(tokenUrl,data, config)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response;
})
.then(response => {
localStorage.setItem('token', response.data);
})
.catch((error) => {
throw new Error('Network error');
});
console.log("LOGIN");
return Promise.resolve();
},
// called when the user clicks on the logout button
logout: () => {
localStorage.removeItem('token');
return Promise.resolve();
},
// called when the API returns an error
checkError: ({ status }) => {
if (status === 401 || status === 403) {
console.log("passato in checkError");
localStorage.removeItem('token');
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
console.log("CHECK AUTH");
return localStorage.getItem('token')
? Promise.resolve()
: Promise.reject();
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => Promise.resolve(),
};
I think you did the wrong returns.
Your login function returns Promise.resolve() right at the moment it emits the axios HTTP POST request, long before it gets the response from the authentication server.
Instead replace the return by of the function by :
return axios.post(tokenUrl,data, config)
.then(response =>
{
if (response.status < 200 || response.status >= 300)
{
throw new Error(response.statusText);
}
return response;
}).then(response =>
{
localStorage.setItem('token', response.data);
})
.catch((error) => {
throw new Error('Network error');
});
This way the Promise resolution will only happens after your authentication server responds.

How can I remove the `GET http://localhost:5000/api/users/profile 401 (Unauthorized)` showing in the console?

I am setting up refresh and access token system for my Vue web application. http://localhost:5000/api/users/profile is the URL of my POST request. I expect an error when someone tries to access and their access token has expired. I use interceptors from Axios in order to generate a brand new access token when such error appears. Everything works fine. However, I've spent a lot of time trying to figure out how to get rid of the GET http://localhost:5000/api/users/profile 401 (Unauthorized) in console. Is there any way to rid of it? Any help would be appreciated.
Getting profile:
getProfile: async (context) => {
context.commit('PROFILE_REQUEST')
let res = await axios.get('http://localhost:5000/api/users/profile')
context.commit('USER_PROFILE', res.data.user);
return res;
}
Interceptor:
axios.interceptors.response.use((res) => {
return res;
}, async (err) => {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = Cookies.get('jid');
return axios.post('http://localhost:5000/api/users/refresh-token', { refreshToken }).then((res) => {
axios.defaults.headers.common['Authorization'] = `Bearer ${res.data.accessToken}`;
originalRequest.headers['Authorization'] = `Bearer ${res.data.accessToken}`;
originalRequest.baseURL = undefined;
return axios(originalRequest);
})
}
return Promise.reject(err);
});
}

How to re-post data to the server after a JWT token has been refreshed in Vuejs/Laravel

I'm building a PWA wherein users log in to enter production data to a form and submit to the server for subsequent processing. I'm using a JWT token to manage the user's status. I'm using Axios interceptors to check that the token is fresh/expired. If the latter, I'm refreshing the token.
My current problem is that I don't know how to automatically resubmit a user's data input if, upon form submission, their token was found to be expired and a new one created.
So, in my bootstrap.js file I have:
window.axios.interceptors.response.use((response) => {
return response;
}, error => {
let pathUrl = error.config.url;
if (error.response.status !== 401) {
return new Promise((resolve, reject) => {
reject(error);
});
}
if (pathUrl == '/api/question' || error.response.message == 'Your session has expired; please log in again.') {
getRefreshToken();
return Promise.reject(error)
}
});
function getRefreshToken() {
window.axios.post('/api/auth/refresh')
.then(response => {
const token = response.data.access_token
localStorage.setItem('token', token)
const JWTtoken = 'Bearer ' + token
window.axios.defaults.headers.common['Authorization'] = JWTtoken;
})
}
The method for submitting the form in the component within which the data are inputted is:
submitData () {
let vm = this;
if (vm.$v.formvar.$pending || vm.$v.formvar.$error) return;
axios.post('/api/question',vm.formvar)
.then(res => {
this.$router.push('/' + this.$i18n.locale + res.data.path)
})
},
Any help here would be gratefully received.
Thanks/Tom
You can try using window.axios.request(error.config) to resend the request
if (pathUrl == '/api/question' || error.response.message == 'Your session has expired; please log in again.') {
return getRefreshToken()
.then(JWTtoken => {
error.config.headers['Authorization'] = JWTtoken
return window.axios.request(error.config)
})
}
getRefreshToken should return a Promise
function getRefreshToken() {
return window.axios.post('/api/auth/refresh')
.then(response => {
const token = response.data.access_token
localStorage.setItem('token', token)
const JWTtoken = 'Bearer ' + token
window.axios.defaults.headers.common['Authorization'] = JWTtoken;
return JWTtoken
})
}

How can I get an axios interceptor to retry the original request?

I am trying to implement a token refresh into my vue.js application. This is working so far, as it refreshes the token in the store on a 401 response, but all I need to do is get the interceptor to retry the original request again afterwards.
main.js
axios.interceptors.response.use(
response => {
return response;
},
error => {
console.log("original request", error.config);
if (error.response.status === 401 && error.response.statusText === "Unauthorized") {
store.dispatch("authRefresh")
.then(res => {
//retry original request???
})
.catch(err => {
//take user to login page
this.router.push("/");
});
}
}
);
store.js
authRefresh(context) {
return new Promise((resolve, reject) => {
axios.get("auth/refresh", context.getters.getHeaders)
.then(response => {
//set new token in state and storage
context.commit("addNewToken", response.data.data);
resolve(response);
})
.catch(error => {
reject(error);
});
});
},
I can log the error.config in the console and see the original request, but does anyone have any idea what I do from here to retry the original request? and also stop it from looping over and over if it fails.
Or am I doing this completely wrong? Constructive criticism welcome.
You could do something like this:
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = window.localStorage.getItem('refreshToken');
return axios.post('http://localhost:8000/auth/refresh', { refreshToken })
.then(({data}) => {
window.localStorage.setItem('token', data.token);
window.localStorage.setItem('refreshToken', data.refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
return axios(originalRequest);
});
}
return Promise.reject(error);
});
Implementation proposed by #Patel Pratik is good but only handles one request at a time.
For multiple requests, you can simply use axios-auth-refresh package. As stated in documentation:
The plugin stalls additional requests that have come in while waiting
for a new authorization token and resolves them when a new token is
available.
https://www.npmjs.com/package/axios-auth-refresh
#Patel Pratik, thank you.
In react native, I've used async storage and had custom http header, server needed COLLECTORACCESSTOKEN, exactly in that format (don't say why =)
Yes, I know, that it shoud be secure storage.
instance.interceptors.response.use(response => response,
async error => { -----it has to be async
const originalRequest = error.config;
const status = error.response?.status;
if (status === 401 && !originalRequest.isRetry) {
originalRequest.isRetry = true;
try {
const token = await AsyncStorage.getItem('#refresh_token')
const res = await axios.get(`${BASE_URL}/tokens/refresh/${token}`)
storeAccess_token(res.data.access_token)
storeRefresh_token(res.data.refresh_token)
axios.defaults.headers.common['COLLECTORACCESSTOKEN'] =
res.data.access_token;
originalRequest.headers['COLLECTORACCESSTOKEN'] =
res.data.access_token;
return axios(originalRequest);
} catch (e) {
console.log('refreshToken request - error', e)
}
}
if (error.response.status === 503) return
return Promise.reject(error.response.data);
});
Building on #Patel Praik's answer to accommodate multiple requests running at the same time without adding a package:
Sorry I don't know Vue, I use React, but hopefully you can translate the logic over.
What I have done is created a state variable that tracks whether the process of refreshing the token is already in progress. If new requests are made from the client while the token is still refreshing, I keep them in a sleep loop until the new tokens have been received (or getting new tokens failed). Once received break the sleep loop for those requests and retry the original request with the updated tokens:
const refreshingTokens = useRef(false) // variable to track if new tokens have already been requested
const sleep = ms => new Promise(r => setTimeout(r, ms));
axios.interceptors.response.use(function (response) {
return response;
}, async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// if the app is not already requesting a new token, request new token
// i.e This is the path that the first request that receives status 401 takes
if (!refreshingTokens.current) {
refreshingTokens.current = true //update tracking state to say we are fething new tokens
const refreshToken = localStorage.getItem('refresh_token')
try {
const newTokens = await anAxiosInstanceWithoutInterceptor.post(`${process.env.REACT_APP_API_URL}/user/token-refresh/`, {"refresh": refreshToken});
localStorage.setItem('access_token', newTokens.data.access);
localStorage.setItem('refresh_token', newTokens.data.refresh);
axios.defaults.headers['Authorization'] = "JWT " + newTokens.data.access
originalRequest.headers['Authorization'] = "JWT " + newTokens.data.access
refreshingTokens.current = false //update tracking state to say new
return axios(originalRequest)
} catch (e) {
await deleteTokens()
setLoggedIn(false)
}
refreshingTokens.current = false //update tracking state to say new tokens request has finished
// if the app is already requesting a new token
// i.e This is the path the remaining requests which were made at the same time as the first take
} else {
// while we are still waiting for the token request to finish, sleep for half a second
while (refreshingTokens.current === true) {
console.log('sleeping')
await sleep(500);
}
originalRequest.headers['Authorization'] = "JWT " +
localStorage.getItem('access_token');
return axios(originalRequest)
}
}
return Promise.reject(error);
});
If you don't want to use a while loop, alternatively you could push any multiple request configs to a state variable array and add an event listener for when the new tokens process is finished, then retry all of the stored arrays.

Return to request after refreshing tokens

I am trying to get refresh tokens working.
It works for the most part, but the problem is I'm not sure how to return to where the original call was before the access token needed refreshing.
It doesn't work the first time it refreshes, after that the token is refreshed and then it works ok again until it expires.
So the problem is I cant get it returning to where it started on the refresh
Here is an example from the component
created(){
axios.get("http://localhost:63861/api/sampledata/WeatherForecasts").then(response => {
console.log(response.data);
//**** DO STUFF WITH THE DATA. I WANT TO GET BACK HERE AFTER REFRESH
})
.catch(error => {
console.log(error);
});
I need to get back to the point where it can do stuff with the data once it has refreshed and reset the access tokens.
my interceptor:
import axios from "axios";
import store from "../store";
import Storage from "../services/storage";
import { REFRESH } from "../store/actions.type";
export default function execute() {
// Request
axios.interceptors.request.use(
config => {
var token = Storage.getAccessToken();
if (token) {
console.log("Bearer " + token);
config.headers["Authorization"] = "Bearer " + token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// Response
axios.interceptors.response.use(
response => {
return response;
},
error => {
console.log("Error need to refresh");
const originalRequest = error.config;
// token expired
if (error.response.status === 401) {
originalRequest._retry = true;
let tokenModel = {
accessToken: Storage.getAccessToken(),
client: "Web",
refreshToken: Storage.getRefreshToken()
};
var refreshPath = "auth/" + REFRESH;
store
.dispatch(refreshPath, { tokenModel })
.then(response => {
console.log(response);
return axios(originalRequest);
})
.catch(error => {
console.log(error);
// Logout
});
}
return Promise.reject(error);
}
);
}
You need to return your refresh promise.
return store
.dispatch(refreshPath, { tokenModel })
.then(response => {
console.log(response);
return axios(originalRequest);
})
.catch(error => {
console.log(error);
// Logout
});
What is happening now is you dispatch the action, then your return Promise.reject(error) is ran. By returning the refresh promise, you ensure axios waits for that chain to finish