Axios interceptors and asynchronous login - javascript

I'm implementing token authentication. My access token expires every N minutes and then a refresh token is used to log in and get a new access token.
I use Axios for my API calls. I have an interceptor set up to intercept 401 responses:
axios.interceptors.response.use(undefined, function (err) {
if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
serviceRefreshLogin(
getRefreshToken(),
success => { setTokens(success.access_token, success.refresh_token) },
error => { console.log('Refresh login error: ', error) }
)
err.config.__isRetryRequest = true
err.config.headers.Authorization = 'Bearer ' + getAccessToken()
return axios(err.config);
}
throw err
})
Basically, as I intercept a 401 response, I want to do a login and then retry the original rejected request with the new tokens. My serviceRefreshLogin function calls setAccessToken() in its then block. But the problem is that
the then block happens later than the getAccessToken() in the interceptor, so the retry happens with the old expired credentials.
getAccessToken() and getRefreshToken() simply return the existing tokens stored in the browser (they manage localStorage, cookies, etc).
How would I go about ensuring statements do not execute until a promise returns?
(Here's a corresponding issue on Github: https://github.com/mzabriskie/axios/issues/266)

Just use another promise :D
axios.interceptors.response.use(undefined, function (err) {
return new Promise(function (resolve, reject) {
if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
serviceRefreshLogin(
getRefreshToken(),
success => {
setTokens(success.access_token, success.refresh_token)
err.config.__isRetryRequest = true
err.config.headers.Authorization = 'Bearer ' + getAccessToken();
axios(err.config).then(resolve, reject);
},
error => {
console.log('Refresh login error: ', error);
reject(error);
}
);
}
throw err;
});
});
If your enviroment doesn't suport promises use polyfill, for example https://github.com/stefanpenner/es6-promise
But, it may be better to rewrite getRefreshToken to return promise and then make code simpler
axios.interceptors.response.use(undefined, function (err) {
if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
return getRefreshToken()
.then(function (success) {
setTokens(success.access_token, success.refresh_token) ;
err.config.__isRetryRequest = true;
err.config.headers.Authorization = 'Bearer ' + getAccessToken();
return axios(err.config);
})
.catch(function (error) {
console.log('Refresh login error: ', error);
throw error;
});
}
throw err;
});
Demo https://plnkr.co/edit/0ZLpc8jgKI18w4c0f905?p=preview

Could do it in the request instead of the response, and it'd probably be cleaner since it'd avoid hitting the server when the access token's expired. Copying from this article:
function issueToken() {
return new Promise((resolve, reject) => {
return client({
...
}).then((response) => {
resolve(response);
}).catch((err) => {
reject(err);
});
});
}
client.interceptors.request.use((config) => {
let originalRequest = config;
if (tokenIsExpired && path_is_not_login) {
return issueToken().then((token) => {
originalRequest['Authorization'] = 'Bearer ' + token;
return Promise.resolve(originalRequest);
});
}
return config;
}, (err) => {
return Promise.reject(err);
});

Related

React + axios interceptors: retry original request within component's context

I'm learning React and I'm using axios and JWT for authentication. I have written an interceptor to refresh the token automatically:
privateAxios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
const { config, response } = error;
const originalRequest = config;
if (response?.status === 401) {
apiProvider
.refreshToken()
.then(() => {
let headers = getAuthHeaders();
privateAxios.defaults.headers = headers;
originalRequest.headers = headers;
return privateAxios(originalRequest);
})
.catch((err) => {
logout();
return Promise.reject(err);
});
}
return Promise.reject(error);
}
);
On my component I have the following:
api.post(data)
.then(() => {
showSuccessFeedbackForm();
reloadTable();
handleClose();
})
.catch((error) => {
setAlertInfos({
message: JSON.stringify(error.response.data),
severity: "error",
});
setShowAlert(true);
})
.finally(() => {
setIsLoaded(true);
});
My problem is that I want to continue with the component's normal "flow" (i.e., showSuccessFeedbackForm() and reloadTable() and handleClose()) if the token needed to be refreshed (when the code reaches return privateAxios(originalRequest)).
How can I accomplish this?
It looks like you should just have to return the apiProvider.refreshToken()... call. After return privateAxios(originalRequest); returns, then return Promise.reject(error); is executing which causes the front-end to receiving an rejection not a resolution.
Consider this intercepted error which does not throw an error to the frontend which still "resolves":
axios.interceptors.response.use(
(res) => res,
(err) => {
console.log("##### AXIOS ERROR #####");
dispatch(increment());
}
);
Simply changing it to this causes the front-end to catch an error which is what your code is essentially doing:
axios.interceptors.response.use(
(res) => res,
(err) => {
console.log("##### AXIOS ERROR #####");
return Promise.reject();
}
);

Promises that require output from other promises

I'm working on some code for an express API that essentially reaches out to an external REST service for an authentication token, and then uses that token to do stuff. I'm very new to NodeJS, so I think I'm having a lot of trouble with the whole sync/async thing.
Right now my main problem seems to be that I'm getting ReferenceError: err is not defined, which seems to be related to the line in library.js, but I expect there are a lot of problems here, and will appreciate anything that can get me back on the right track.
index.js
library = require('library.js');
module.exports = async (req,res) => {
// This is a test endpoint for prototyping code and testing calls.
URI = "/some/uri";
method = "GET";
body = "";
try {
restResponse = await library.RESTCall(URI,method,body);
res.send(data);
} catch (e) {
return res.status(500).json({ errors: err});
}
};
library.js
exports.RESTCall = async function(URI,method,body) {
return new Promise((resolve, reject) => {
getAuthToken().then((token) => {
console.log("Token: " + token);
try {
// Do stuff with the token to make another call
resolve(data);
} catch (e) {
reject(e);
}
}).catch((err) => {
reject(err);
});
});
}
exports.getAuthToken = () => {
return new Promise((resolve, reject) => {
try {
// Do stuff to get an authentication token
resolve(authToken);
} catch(e) {
reject("Failed to get Facets Authentication token. Error: " + e);
}
});
}
This looks like just a typo:
return res.status(500).json({ errors: e});
FYI this:
exports.RESTCall = async function(URI,method,body) {
return new Promise((resolve, reject) => {
getAuthToken().then((token) => {
console.log("Token: " + token);
try {
// Do stuff with the token to make another call
resolve(data);
} catch (e) {
reject(e);
}
}).catch((err) => {
reject(err);
});
});
}
Is mostly equivalent, but slightly worse as:
exports.RESTCall = function(URI,method,body) {
return getAuthToken().then((token) => {
console.log("Token: " + token);
// Do stuff with the token to make another call
return data;
}
}
But because you have async/await, can be simplified further:
exports.RESTCall = async function(URI,method,body) {
const token = await getAuthToken();
console.log("Token: " + token);
// Do stuff with the token to make another call
return data;
}
Every time you see yourself type new Promise, consider it a red flag. I'd really suggest you take the time to learn how promises work.

How can return false value promise method in node if array is empty and vise versa

i was trying out promise code but it always returns me resolve even if the user does not exist in the database
can anyone help me fix my code and the return statement
in the return function the the second console log is only working.
here is my code
Api Call
const email = 't#t.com';
const request = require('request');
function IsUserExists(email, kc_accessToken) {
let url = `${path}/users?email=${email}`;
return new Promise(function (resolve, reject) {
request(
{
url: url,
headers: {
'content-type': 'application/json',
authorization: `Bearer ${kc_accessToken}`,
},
},
function (error, response, body) {
if (error) {
console.log('some error occured');
}
if (response.body.length > 0) {
console.log('User Exist');
return resolve();
}
console.log('Does not Exist');
return reject();
}
);
});
}
Function Call
http
.createServer(function Test() {
getAccessToken()
.then(function (response) {
kc_accessToken = response.data.access_token;
IsUserExists(email, kc_accessToken).then((resp) => {
if (resp) {
console.log('Do Not Create');
} else if (!resp) {
console.log('Creat a new User');
}
});
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
})
.listen(8081);
When Provided user email which exist ( t#t.com )
When Provided user email which does not exist( 09#t.com )
I need to create a new answer for example to you question in comments.
Now, you go into the reject function so you need to handle this rejection in the outside.
if (response.body.length > 0) {
console.log('User Exist');
return resolve();
}
console.log('Does not Exist');
return reject(); // -> Now here you are
You need add .catch function after IsUserExists.then().
It will be IsUserExists.then().catch()
http.createServer(function Test() {
getAccessToken()
.then(function (response) {
kc_accessToken = response.data.access_token;
// here you only accept the data from resolve in Promise
// so you need to add .catch function to handle the rejection.
IsUserExists(email, kc_accessToken).then((resp) => {
if (resp) {
console.log('Do Not Create');
} else if (!resp) {
console.log('Creat a new User');
}
}).catch((error) => {
console.log(error)
});
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
})
.listen(8081);
By the way, you could add parameter in rejection function like reject(new Error("user not found)).
Then in the outside, you can get this rejection message.

How to handle Promise that returns a 404 status?

I have a method that uses node-fetch to make a POST call to update a profile object in a table via an API. If an invalid profileId is provided (status 404) the promise still resolves. What's the best way to handle it so that I can only accept status 200? The method is defined as:
async function updateUserProfileSocketId(profileId, socketId) {
const body = { id: profileId, socketId };
try {
const response = await fetch(`${API_URL}/updateUserProfile`, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
});
if (response.status !== 200) {
throw new Error(response.status);
}
} catch (err) {
console.log(`updateUserProfileSocketId Error: ${err}`);
}
}
And the method is called in a service class like this:
onInit(socket) {
socket.on('init', (profile) => {
Promise.resolve(updateUserProfileSocketId(profile.id, socket.id))
.then((response) => {
if (response === null || response === undefined) {
console.log(`Unable to find profile ${profile.id}`);
socket.conn.close();
} else {
users.push(profile.id);
}
})
.catch((err) => {
console.log(err);
});
});
}
This seems to work, but I'm not sure if this is the best way to handle this. Any ideas?
If the response status is not 200, you throw an exception that will immediately be caught again. This is probably not what you want. You can leave the catch block for logging purposes, but you should rethrow the exception:
async function updateUserProfileSocketId(profileId, socketId) {
const body = { id: profileId, socketId };
try {
const response = await fetch(...);
if (response.status !== 200) {
throw new Error(response.status);
}
} catch (err) {
console.log(`updateUserProfileSocketId Error: ${err}`);
throw err;
}
}
The same thing applies to the catch-handler inside the socket-callback.
However, removing the try/catch/log/rethrow logic and handling the exception centrally would be cleaner.

How can we maintain user logged in when access token expires and we need to login again to continue as normal user

I'm using Nuxt-axios module with the proxy.
For Error handling, I have common code in
Plugins/axios.js
export default function({ $axios, __isRetryRequest, store, app, redirect , payload , next}) {
$axios.onRequest(config => {
if (app.$cookies.get('at') && app.$cookies.get('rt') && config.url != '/post_login/') {
config.headers.common['Authorization'] = `Bearer ${app.$cookies.get('at')}`;
}
});
$axios.onResponseError(err => {
const code = parseInt(err.response && err.response.status)
let originalRequest = err.config;
if (code === 401) {
originalRequest.__isRetryRequest = true;
store
.dispatch('LOGIN', { grant_type: 'refresh_token', refresh_token: app.$cookies.get('rt')})
.then(res => {
originalRequest.headers['Authorization'] = 'Bearer ' + app.$cookies.get('at');
return app.$axios(originalRequest);
})
.catch(error => {
console.log(error);
});
}
// code for 422 error
if (code == 422) {
throw err.response;
}
});
}
On my page folder index page
Pages/index.vue
<template>
<section>Component data</section>
</template>
<script type="text/javascript">
export default {
async asyncData({ route, store }) {
await store.dispatch('GET_BANNERS');
}
}
</script>
All the API calls are in a stroes/actions.js file.
Now the question is when I refresh the page index.vue first API request will hit and get the response if successful. But now if on first request( 'GET_BANNERS' ) from asyncData and it gets 401 error unauthorized then I'm getting below error
Error: Request failed with status code 401
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
how can I resolve this?
few more questions:
1) When I'm writing common error code in axios, original request on which I have received 401 how can I set data to store again(which we normally do from actions file)?
2) can anyone help with best practice to attach authorization headers and error handle for 400,401,422, etc..
$axios.onResponseError(err => {
const code = parseInt(err.response && err.response.status);
let originalRequest = err.config;
if (code == 401) {
originalRequest.__isRetryRequest = true;
let token = app.$cookies.get('rt');
return new Promise((resolve, reject) => {
let req = $axios
.post(`/login`, { grant_type: 'refresh_token', refresh_token: token })
.then(response => {
if (response.status == 200) {
app.$cookies.set('access', response.data.access_token);
app.$cookies.set('refresh', response.data.refresh_token);
originalRequest.headers['Authorization'] = `Bearer ${
response.data.access_token
}`;
}
resolve(response);
}).catch(e => {
reject("some message");
})
})
.then(res => {
return $axios(originalRequest);
}).catch(e => {
app.router.push('/login');
});
}
});
#canet-robern hope this will solve your prob!!
The error ERR_HTTP_HEADERS_SENT means that you have a bug in your server-side code - hence the error from this bug comes before the HTTP headers.
To handle 4xx errors and retry the Axios request - follow this example:
Vue.prototype.$axios = axios.create(
{
headers:
{
'Content-Type': 'application/json',
},
baseURL: process.env.API_URL
}
);
Vue.prototype.$axios.interceptors.request.use(
config =>
{
events.$emit('show_spin');
let token = getTokenID();
if(token && token.length) config.headers['Authorization'] = token;
return config;
},
error =>
{
events.$emit('hide_spin');
if (error.status === 401) VueRouter.push('/login');
else throw error;
}
);
Vue.prototype.$axios.interceptors.response.use(
response =>
{
events.$emit('hide_spin');
return response;
},
error =>
{
events.$emit('hide_spin');
return new Promise(function(resolve,reject)
{
if (error.config && error.response && error.response.status === 401 && !error.config.__isRetry)
{
myVue.refreshToken(function()
{
error.config.__isRetry = true;
error.config.headers['Authorization'] = getTokenID();
myVue.$axios(error.config).then(resolve,reject);
},function(flag) // true = invalid session, false = something else
{
if(process.env.NODE_ENV === 'development') console.log('Could not refresh token');
if(getUserID()) myVue.showFailed('Could not refresh the Authorization Token');
reject(flag);
});
}
else throw error;
});
}
);

Categories

Resources