I am building a SPA in Vue, and using axios interceptors to handle token management. Now, the SPA is not refreshing the token manually, it is receiving the token from the server only when it is refreshed, and then I update localStorage with the new token. I am passing the token in the headers on every API call.
My problem is that when the token comes back after it's been refreshed, I update localStorage in the response interceptor. But subsequent API calls are not aware of this new value in the store.
How can I retry the requests with the new value in localStorage?
I have tried, in the error block of the response interceptor, to grab the value from localStorage and manually update the headers and return the original request but this doesn't seem to work, as the subsequent API calls fail still with the old value.
axios.interceptors.request.use(config => {
const accessToken = window.localStorage.getItem('authToken')
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`
}
return Promise.resolve(config)
})
axios.interceptors.response.use(
response => {
if (response.data.meta && response.data.meta.tokens && response.data.meta.tokens.Bearer) {
const token = response.data.meta.tokens.Bearer
console.log({ 'setting new token': token })
window.localStorage.setItem('authToken', token)
}
return response
},
error => {
console.log(error)
const originalRequest = error.config
if (error.status && error.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const accessToken = window.localStorage.getItem('authToken')
originalRequest.headers.Authorization = `Bearer ${accessToken}`
return axios(originalRequest)
}
return Promise.reject(error)
}
)
Related
In my React Native app, I use Axios Interceptors to set an Auth header Token on every request. Everything works correctly but when I log out and clear the stored token (By PURGING the Redux store) and log in again, the new token doesn't get set on the Axios Authorization Header (Gets empty).
This is how I'm setting the Auth header on Axios with Interceptors:
const App = () => {
const token = useSelector(selectToken); // Token state
const isLoggedOut = useSelector(selectIsLoggedOutState); // User logout state
// Show logout message and purge the store
useEffect(() => {
if (isLoggedOut) {
Toast.show({
type: 'success',
text1: logoutMessage,
});
persistor.purge();
}
}, [isLoggedOut]);
// Intercept on request
api.interceptors.request.use(
config => {
// Set authorization header
if (token) {
config.headers.common['Authorization'] = 'Bearer ' + token;
}
return config;
},
err => {
return Promise.reject(err);
},
);
// REST OF THE CODE
}
The most weird thing is that the header Token gets changed even if token is false in the if block, as if it doesn't even get checked.
I'd really appreciate any answer that might help me fix this weird problem.
API request using JWT is implemented in flask and Vue.js.
The JWT is stored in a cookie, and the server validates the JWT for each request.
If the token has expired, a 401 error will be returned.
f you receive a 401 error, refresh the token as in the code below,
The original API request is made again.
The following code is common to all requests.
http.interceptors.response.use((response) => {
return response;
}, error => {
if (error.config && error.response && error.response.status === 401 && !error.config._retry) {
error.config._retry = true;
http
.post(
"/token/refresh",
{},
{
withCredentials: true,
headers: {
"X-CSRF-TOKEN": Vue.$cookies.get("csrf_refresh_token")
}
}
)
.then(res => {
if (res.status == 200) {
const config = error.config;
config.headers["X-CSRF-TOKEN"] = Vue.$cookies.get("csrf_access_token");
return Axios.request(error.config);
}
})
.catch(error => {
});
}
return Promise.reject(error);
});
When making multiple API requests at the same time with the token expired
Uselessly refreshing the token.
For example, requests A, B, and C are executed almost simultaneously.
Since 401 is returned with each request,
Each interceptor will refresh the token.
There is no real harm, but I don't think it's a good way.
There is a good way to solve this.
My idea is to first make an API request to validate the token expiration,
This method is to make requests A, B, and C after verification and refresh are completed.
Because cookies are HttpOnly, the expiration date cannot be verified on the client side (JavaScript).
Sorry in poor english...
What you'll need to do is maintain some state outside the interceptor. Something that says
Hold up, I'm in the middle of getting a new token.
This is best done by keeping a reference to a Promise. That way, the first 401 interceptor can create the promise, then all other requests can wait for it.
let refreshTokenPromise // this holds any in-progress token refresh requests
// I just moved this logic into its own function
const getRefreshToken = () => http.post('/token/refresh', {}, {
withCredentials: true,
headers: { 'X-CSRF-TOKEN': Vue.$cookies.get('csrf_refresh_token') }
}).then(() => Vue.$cookies.get('csrf_access_token'))
http.interceptors.response.use(r => r, error => {
if (error.config && error.response && error.response.status === 401) {
if (!refreshTokenPromise) { // check for an existing in-progress request
// if nothing is in-progress, start a new refresh token request
refreshTokenPromise = getRefreshToken().then(token => {
refreshTokenPromise = null // clear state
return token // resolve with the new token
})
}
return refreshTokenPromise.then(token => {
error.config.headers['X-CSRF-TOKEN'] = token
return http.request(error.config)
})
}
return Promise.reject(error)
})
I have an vue.js SPA application. My goal is to refresh the token if it was expired via axios interceptors. When user sends the request to api, I need to check token expire time at first, and if it was expired - refresh it and then complete user's request.
I got an refresh function:
const refreshToken = () => {
return new Promise((resolve, reject) => {
return axios.post('/api/auth/token/refresh/').then((response) => {
resolve(response)
}).catch((error) => {
reject(error)
})
})
}
And axios request interceptor:
axios.interceptors.request.use((config) => {
let originalRequest = config
if (jwt.isTokenExpired()) {
return api.refreshToken()
.then(res => {
if (res.data.error == 'TOKEN_BLACKLISTED' && res.headers.authorization) {
//get the token from headers without "Bearer " word
let token = res.headers.authorization.slice(7)
//save the token in localStorage
jwt.setToken(`"${token}"`)
//refresh "Authorization" header with new token
api.setHeader()
return Promise.resolve(originalRequest)
} else {
jwt.destroyToken()
jwt.destroyExpiredTime()
store.dispatch('auth/destroyToken')
router.push({name: 'login'})
return Promise.reject()
}
})
}
return config
}, (err) => {
return Promise.reject(err)
})
But now it goes to infinite loop. How to fix it?
In this case, you'd better make two instances of axios:
the first for authorization-related endpoints (those that do not require an access token), for example, axiosAuth.
In your example - axiosAuth.post('/api/auth/token/refresh/')
the second for the authorized part of your project, for example axiosApi.
In your example - axiosApi.interceptors.request.use
You must install the interceptor for the second instance, in this case the call to refresh_token will not trigger the interceptor in which it is installed, as you would expect
You are making a request in the interceptor. Which means that the token is stil expired when the interceptor is called on the request to the refresh url. So what you could do is to check in your interceptor if the URL is set to your refresh token URL and then just resolve the original request.
I have an interceptor in place to catch 401 errors if the access token expires. If it expires it tries the refresh token to get a new access token. If any other calls are made during this time they are queued until the access token is validated.
This is all working very well. However when processing the queue using Axios(originalRequest) the originally attached promises are not being called. See below for an example.
Working interceptor code:
Axios.interceptors.response.use(
response => response,
(error) => {
const status = error.response ? error.response.status : null
const originalRequest = error.config
if (status === 401) {
if (!store.state.auth.isRefreshing) {
store.dispatch('auth/refresh')
}
const retryOrigReq = store.dispatch('auth/subscribe', token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token
Axios(originalRequest)
})
return retryOrigReq
} else {
return Promise.reject(error)
}
}
)
Refresh Method (Used the refresh token to get a new access token)
refresh ({ commit }) {
commit(types.REFRESHING, true)
Vue.$http.post('/login/refresh', {
refresh_token: store.getters['auth/refreshToken']
}).then(response => {
if (response.status === 401) {
store.dispatch('auth/reset')
store.dispatch('app/error', 'You have been logged out.')
} else {
commit(types.AUTH, {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token
})
store.dispatch('auth/refreshed', response.data.access_token)
}
}).catch(() => {
store.dispatch('auth/reset')
store.dispatch('app/error', 'You have been logged out.')
})
},
Subscribe method in auth/actions module:
subscribe ({ commit }, request) {
commit(types.SUBSCRIBEREFRESH, request)
return request
},
As well as the Mutation:
[SUBSCRIBEREFRESH] (state, request) {
state.refreshSubscribers.push(request)
},
Here is a sample action:
Vue.$http.get('/users/' + rootState.auth.user.id + '/tasks').then(response => {
if (response && response.data) {
commit(types.NOTIFICATIONS, response.data || [])
}
})
If this request was added to the queue I because the refresh token had to access a new token I would like to attach the original then():
const retryOrigReq = store.dispatch('auth/subscribe', token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token
// I would like to attache the original .then() as it contained critical functions to be called after the request was completed. Usually mutating a store etc...
Axios(originalRequest).then(//if then present attache here)
})
Once the access token has been refreshed the queue of requests is processed:
refreshed ({ commit }, token) {
commit(types.REFRESHING, false)
store.state.auth.refreshSubscribers.map(cb => cb(token))
commit(types.CLEARSUBSCRIBERS)
},
Update Feb 13, 2019
As many people have been showing an interest in this topic, I've created the axios-auth-refresh package which should help you to achieve behaviour specified here.
The key here is to return the correct Promise object, so you can use .then() for chaining. We can use Vuex's state for that. If the refresh call happens, we can not only set the refreshing state to true, we can also set the refreshing call to the one that's pending. This way using .then() will always be bound onto the right Promise object, and be executed when the Promise is done. Doing it so will ensure you don't need an extra queue for keeping the calls which are waiting for the token's refresh.
function refreshToken(store) {
if (store.state.auth.isRefreshing) {
return store.state.auth.refreshingCall;
}
store.commit('auth/setRefreshingState', true);
const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
store.commit('auth/setToken', token)
store.commit('auth/setRefreshingState', false);
store.commit('auth/setRefreshingCall', undefined);
return Promise.resolve(true);
});
store.commit('auth/setRefreshingCall', refreshingCall);
return refreshingCall;
}
This would always return either already created request as a Promise or create the new one and save it for the other calls. Now your interceptor would look similar to the following one.
Axios.interceptors.response.use(response => response, error => {
const status = error.response ? error.response.status : null
if (status === 401) {
return refreshToken(store).then(_ => {
error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
error.config.baseURL = undefined;
return Axios.request(error.config);
});
}
return Promise.reject(error);
});
This will allow you to execute all the pending requests once again. But all at once, without any querying.
If you want the pending requests to be executed in the order they were actually called, you need to pass the callback as a second parameter to the refreshToken() function, like so.
function refreshToken(store, cb) {
if (store.state.auth.isRefreshing) {
const chained = store.state.auth.refreshingCall.then(cb);
store.commit('auth/setRefreshingCall', chained);
return chained;
}
store.commit('auth/setRefreshingState', true);
const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
store.commit('auth/setToken', token)
store.commit('auth/setRefreshingState', false);
store.commit('auth/setRefreshingCall', undefined);
return Promise.resolve(token);
}).then(cb);
store.commit('auth/setRefreshingCall', refreshingCall);
return refreshingCall;
}
And the interceptor:
Axios.interceptors.response.use(response => response, error => {
const status = error.response ? error.response.status : null
if (status === 401) {
return refreshToken(store, _ => {
error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
error.config.baseURL = undefined;
return Axios.request(error.config);
});
}
return Promise.reject(error);
});
I haven't tested the second example, but it should work or at least give you an idea.
Working demo of first example - because of the mock requests and demo version of service used for them, it will not work after some time, still, the code is there.
Source: Interceptors - how to prevent intercepted messages to resolve as an error
Why not try something like this ?
Here I use AXIOS interceptors in both directions. For the outgoing direction I set the Authorization header. For the incoming direction - if there is an error, I return a promise (and AXIOS will try to resolve it). The promise checks what the error was - if it was 401 and we see it for the first time (i.e. we are not inside the retry) then I try to refresh the token. Otherwise I throw the original error.
In my case refreshToken() uses AWS Cognito but you can use whatever suits you most. Here I have 2 callbacks for refreshToken():
when the token is successfully refreshed, I retry the AXIOS request using an updated config - including the new fresh token and setting a retry flag so that we do not enter an endless cycle if the API repeatedly responds with 401 errors. We need to pass the resolve and reject arguments to AXIOS or otherwise our fresh new promise will be never resolved/rejected.
if the token could not be refreshed for any reason - we reject the promise. We can not simply throw an error because there might be try/catch block around the callback inside AWS Cognito
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'); // probably not needed
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;
});
}
);
This could be done with a single interceptor:
let _refreshToken = '';
let _authorizing: Promise<void> | null = null;
const HEADER_NAME = 'Authorization';
axios.interceptors.response.use(undefined, async (error: AxiosError) => {
if(error.response?.status !== 401) {
return Promise.reject(error);
}
// create pending authorization
_authorizing ??= (_refreshToken ? refresh : authorize)()
.finally(() => _authorizing = null)
.catch(error => Promise.reject(error));
const originalRequestConfig = error.config;
delete originalRequestConfig.headers[HEADER_NAME]; // use from defaults
// delay original requests until authorization has been completed
return _authorizing.then(() => axios.request(originalRequestConfig));
});
The rest is an application specific code:
Login to api
Save/load auth data to/from storage
Refresh token
Check out the complete example.
How to create middleware which will catch all errors, for example I have request which required token, token can expired or damaged, so I need catch this errors on every request and be able to call queries and mutations.
For example:
On expired token, I must refetch token and repeat request.
On token damaged, I must logout user and refetch all queries.
And type of error witch I need to handle can be many.
In(react-apollo docs)
networkInterface.useAfter([{
applyAfterware({ response }, next) {
if (response.status === 401) {
logout();
}
next();
}
}]);
I can't access to graphql error, and call queries or mutations.
You can check to see if you have a token before every request is sent. If you do not have a token, you should handle that somewhere else in your application or potentially fetch another straight from the middleware function. You could make higher order component that wraps all of your components that must have a token. If for some reason there is no token, you can fetch another one and store it to localStorage if you are using the browser or asyncstorage if you are using React Native. Once you've assigned it to localStorage or asyncStorage, this middleware code snippet below will check for the token before every request you send, this includes all queries and mutations. If you find that your user doesn't have a token, you could also redirect them in your component them to a page where they must login again and then from there set the token to localstorage or asynstorage. Once again the apollo client's middleware will have access to it that way.
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { checkForSessionToken } from '../../utils/authentication';
const networkInterface = createNetworkInterface({
uri: 'https://localhost:4000'
});
networkInterface.use([{
applyMiddleware(req, next) {
// Create the header object if needed.
if (!req.options.headers) {
req.options.headers = {};
}
// get the authentication token from Async storage
// and assign it to the request object
checkForSessionToken()
.then(SESSION_TOKEN => {
if (SESSION_TOKEN === null || SESSION_TOKEN === undefined {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', SESSION_TOKEN);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
} else {
req.options.headers.Authorization = `Bearer ${SESSION_TOKEN}`;
}
next();
})
.catch(error => {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', token);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
next();
})
}
}]);
const client = new ApolloClient({
networkInterface,
dataIdFromObject: o => o.id
});
export default client;