axios-jwt
axios-jwt copied to clipboard
How to deal with refresh tokens that are expired?
Thanks for creating this cool library!
I was wondering how to deal with expired refresh tokens? Currently, there seems to be no way to handle this. Is it possible to specify a force-logout URL in case the refresh fails with a 401 or 403?
Thanks!
Like a callback? Not really but feel free to send a PR!
That was a fast response :-D Thanks!
I just saw that it's probably easiest to just add a catch statement to the requestRefetch
function and do a force logout from there.
@florianmartens additionally you could set up your backend so that it also refreshes and returns refresh tokens every time you refresh your access token. This way a user is logged in for as long as they use your (web) app every now and then (depending on your refresh token expiration)
https://github.com/jetbridge/axios-jwt/issues/16
@mvanroon It still leaves a problem that the refresh token will expire at some point in the future, and the call to the backend will throw an exception that will not be handled by the catch()
method.
@mvanroon It still leaves a problem that the refresh token will expire at some point in the future, and the call to the backend will throw an exception that will not be handled by the
catch()
method.
That’s a good thing right? It will cause the axios call to throw an error which you can catch. If the status code is 401 you navigate the user to the login page
@mvanroon I get an error in the console even though I chained a catch()
to the call. I'm assuming that's because the interceptor uses a different axios client instance to refresh the tokens. Maybe I'm missing something...
When I create my refresh function llike this:
const requestRefresh = (refresh_token) => {
return axios.post(`${appConfig.base_url}/api/v1/auth/refresh_token`, { refresh_token })
.then(response => {
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token
};
}).catch (e => Promise.reject(e));
};
Then it sort of works, but the catch()
method on my api call does not receieve a status code of 401. It receives an exception that looks like:
Error: Unable to refresh access token for request due to token refresh error: Got 401 on token refresh; clearing both auth tokens
authTokenInterceptor authTokenInterceptor.ts:209
step axios-jwt.js:2449
verb axios-jwt.js:2396
rejected axios-jwt.js:2374
promise callback*step axios-jwt.js:2380
__awaiter axios-jwt.js:2382
__awaiter axios-jwt.js:2364
authTokenInterceptor authTokenInterceptor.ts:182
I would expect to get an authorization error and redirect the user to the login page. Is there a way to return the refresh token endpoint's response from the interceptor, and handle it as if I don't have a token in the client. I am unauthorized, it should be handled in the same manner.
Thanks for the detailed response. I suppose we should re-throw the exception received from the refresh call and alter the message (prefixing it with ‘Unable to refresh token:’) instead of throwing an entirely new exception. This should be the general idea imo:
async submitForm(values) {
try {
await axiosInstance.post(‘/authorized-endpoint’, values)
} catch (error) {
if (error.response.status === 401) {
navigate(‘/login’)
} else {
// handle other exceptions
}
}
}
An alternative would be to allow one to specify a callback that would be called (onRefreshError
) when refreshing fails.
applyAuthTokenInterceptor(axiosInstance, {
requestRefresh,
onRefreshError: (error) => {
navigate(‘/login’)
}
}
})
Or both :-)
@mvanroon The problem is that the exception thrown when I try to fetch something from the api does not have error.response.status === 401
because the exception is thrown from the interceptor, without returning the error response from the token refresh endpoint. That sort of makes sense since that endpoint is not the endpoint I'm sending a request to.
The alternative solution would probably work, but I find it sort of icky to inject my router into my rest api client initialization, and not informing the caller of what happened to the request.
For now, I handle all the other statuses first, and than in the end, I just assume that the error was an authorization error.
this.getData()
.then((m) => console.log(m))
.catch((e) => {
if (e.response.status === 404) {
alert("Resource not found");
} else if (e.response.status === 403) {
alert("You are not allowed to do that");
} else {
// Here I just assume that the user is not logged in
this.$router.push({
name: "login",
query: {
redirect: this.$route.fullPath,
},
});
}
});
Also icky, but the least icky option, I think. I with I could just get an exception with the 401 response.
I am dealing with the same problem right now. This is what my requestRefresh function looked like at first:
async requestRefresh(refresh) {
// Notice that this is the global axios instance!
const response = await axios.post('/api/token/refresh', { refresh_token: refresh });
return response.data.token;
};
The problem was that when the backend returned a 401 because the refresh token expired, the following exception came up and my application didn't load properly:
Uncaught (in promise) Error: Unable to refresh access token for request due to token refresh error: Got 401 on token refresh; clearing both auth tokens
at authTokenInterceptor.js:247:1
at step (authTokenInterceptor.js:33:1)
at Object.throw (authTokenInterceptor.js:14:46)
at rejected (authTokenInterceptor.js:6:42)
Now I have changed my code to this:
async requestRefresh(refresh) {
try {
// Notice that this is the global axios instance!
const response = await axios.post('/api/token/refresh', { refresh_token: refresh });
return response.data.token;
} catch (error) {
if (error.response && error.response.status === 401) {
this.logout();
}
}
};
This looks better now. The logic of my "logout" function is executed and my application loads as I expect it to. However, the following exception still appears in the console:
Uncaught (in promise) Error: Unable to refresh access token for request due to token refresh error: Failed to refresh auth token: requestRefresh must either return a string or an object with an accessToken
at authTokenInterceptor.js:247:1
at step (authTokenInterceptor.js:33:1)
at Object.throw (authTokenInterceptor.js:14:46)
at rejected (authTokenInterceptor.js:6:42)
The exception occurs because I do not make a return in case of an error, since there is nothing I can return. Returning "null" doesn't work either. I think the code could be improved to also accept a "null" and handle it accordingly.