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.
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 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)
}
)
I have some code set up that refreshes JWT tokens successfully. A problem arises when the user opens multiple tabs, and they all trigger to refresh the tokens at the same time. Each tab gets new tokens that are different from each other and only the latest one will actually work. How can I run the token refresh function once across all browser tabs?
I figured this out. First think first you should store your token in local storage. Then when you request refresh token you should set first the Authorization header with the token in local storage then request it to the server. this is mandatory for getting up to date token. after you request, you will get response new token from server. set the new token to local storage and set the Authorization header (as default) with the new token. This way works for me.
created() {
const vm = this;
axios.interceptors.response.use(
function(response) {
if (response.headers.authorization != undefined) {
localStorage.setItem(
"token",
response.headers.authorization.replace("Bearer ", "")
);
axios.defaults.headers.common["Authorization"] =
response.headers.authorization;
}
return response.data;
},
function(error) {
if (error.response.status == 401) {
vm.$store.dispatch("logout").then(() => {
this.$router.push("/login").catch(err => {});
});
}
return Promise.reject(error);
}
);
},
async mounted() {
while (this.$store.getters.isLoggedIn) {
await new Promise(resolve => setTimeout(resolve, 60000)).then(v => {
axios.defaults.headers.common["Authorization"] =
"Bearer " + localStorage.getItem("token");
axios({ method: "get", url: "/login/refresh" });
});
}
}
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;