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

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
})
}

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);
});
}

response of axios retry in main.js in 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)
}
})
)

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.

react-native - check expiration of jwt with redux-thunk middleware before every call to API

For my react-native app I need to make sure that before every fetch request to server the use-case below should be executed
-> check the expire date of token that is saved to redux.
--> If token is not expired, app keeps going on with requested fetch to server
--> If token expired, app immediately makes new request to refresh token without making user knows it. After successfully refreshing token, app keeps going on with requested fetch to server
I tried to implement middleware with redux-thunk, but I do not know whether it's good design or not. I just need someone experienced with redux and react to give me feedback over my middleware code.
This is how I make requests to server oveer my app's component through dispatching the checkTokenAndFetch - action creater.
url = "https://———————";
requestOptions = {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + this.props.token
}
};
dispatch(authActions.checkTokenAndFetch(url, requestOptions))
.then((data) => {
})
here is action creator - checkTokenAndFetch located in authActions.js
file where my actions located
function checkTokenAndFetch(url, requestOptions){
return dispatch => {
if(authServices.isTokenExpired()){
console.log("TOKEN EXPIRED");
authServices.refreshToken()
.then(
refreshToken => {
var arr = refreshToken.split('.');
decodedToken = base64.decode(arr[1]);
newTokenExpDate = JSON.parse(decodedToken).exp;
dispatch(writeTokenToRedux(refreshToken,newTokenExpDate));
},
error => {
Alert.alert("TOKEN refresh failed","Login Again");
Actions.login();
}
);
}
else{
console.log("TOKEN IS FRESH");
}
return authServices.fetchForUFS(url, requestOptions)
.then(
response => {
return response;
},
error => {
}
)
;
}
}
Here is isTokenExpired and refreshToken functions that I call for case of token expire, located in another file named authServices.js.
function isTokenExpired(){
var newState = store.getState();
var milliseconds = (new Date).getTime();
var exDate = newState.tokenExpDate;
return milliseconds>exDate*1000
}
function refreshToken(){
var refreshToken = store.getState();
return fetch('https://—————————', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + refreshToken.token
}
})
.then((response) => {
return response._bodyText;
})
.catch((error) => {
return error;
})
}
and my fetchForUFS function in authServices.js to make a call to server after completeing token-check(refresh) stuff.
function fetchForUFS(url,requestOptions){
return fetch(url, requestOptions)
.then((response) => {
return response.json();
})
.then((responseData) =>{
return responseData;
})
.catch((error) => {
})
}
I've read tons of redux-thunk, redux-promise and middleware documentation and I'm yet not sure whether I am implementing middleware logic truly?