We are implementing a token-based authentication and when a user signs in we generate access and refresh tokens then save that with the timestamp on device so we can later check if the access token is expired or not.
We are currently using axios interceptor before every request and checking if the token is still valid or not with the timestamp we saved earlier when we generated the access and refresh tokens, but when the access token is expired and we are making a request to refresh the token the app goes on an infinite loop and none of the requests go through (both the original and refresh token api requests). you can see my code below:
const instance = axios.create({
baseURL: 'http://localhost:8080'
});
const refreshToken = () => {
return new Promise((resolve, reject) => {
instance
.post('/token/renew')
.then(response => {
resolve('refresh successful');
})
.catch(error => {
reject(Error(`refresh fail: ${error}`));
});
});
};
instance.interceptors.request.use(
async config => {
const timestamp = 1602155221309;
const diffMinutes = Math.floor(Math.abs(Date.now() - timestamp) / 60000);
// if diffMinutes is greater than 30 minutes
if (diffMinutes > 30) {
const tokenResponse = await refreshToken();
return config;
}
return config;
},
error => {
return Promise.reject(error);
}
);
The infinite loop is caused by your interceptor triggering another Axios request on which said interceptor will also run and trigger another Axios request, forever.
A simple solution would be to make the refresh token network request using the default axios instance which doesn't include any interceptors:
const refreshToken = () => {
// You can skip returning a new `Promise` since `axios.post` already returns one.
return axios.post("YOUR_BASE_URL/token/renew");
};
Obviously that also means you'll have to write a bit of logic to send the current refresh token along if that's included in your instance interceptors.
Related
I'm working on a React native application and I need to set Authentication header on my sensitive requests.
But my accessToken (JWT) expires after 10 seconds. So before any request I have to check that if the token is expired renew it using a refreshToken and then call last request again.
I'm able to do all of these except last part (bold text).
And I'm using Axios for sending request to server.
Any idea?
So the idea is to use a response interceptor that works after the response is available, but before it is passed down in the code.
It looks for unauthenticated error, which corresponds to statusCode 401
Keep in mind that this is a pseudo-code and You have to modify some parts like
auth.setToken().
const createAxiosResponseInterceptor = () => {
const interceptor = axios.interceptors.response.use(
(response) => response,
(error) => {
// Reject promise if not 401 error
if (error.response.status !== 401) {
return Promise.reject(error);
}
/*
* When response code is 401, try to refresh the token.
* Remove the interceptor so it doesn't loop in case
* token refresh causes the 401 response
*
* Also eject the interceptor to prevent it from working again if the REFRESH request returns 401 too
*/
axios.interceptors.response.eject(interceptor);
return axios({
url: API_REFRESH_URL,
method: "POST",
withCredentials: true,
})
.then((response) => {
auth.setToken(response.data.access_token);
error.response.config.headers["Authorization"] =
"Bearer " + response.data.access_token;
return axios(error.response.config);
})
.catch((error) => {
auth.removeToken();
return Promise.reject(error);
})
.finally(createAxiosResponseInterceptor);
}
);
};
createAxiosResponseInterceptor();
error.response.config contains all the data about the old request so we can repeat it. Keep in mind that after completing the Refresh request we again apply the interceptor in .finally(create...Interceptor)
For more details please see this question, and official Docs here
I am quite new to vue and I am trying to send a request to my api using axios.
I build an interceptor which seems to work (logging is happening)
export default function setup() {
console.log('Http interceptor starting...')
Axios.interceptors.request.use((request) => {
const token = store.getters.token;
if (token) {
request.headers.Authorization = `Bearer ${token}`;
}
console.log(request);
return request
}, (err) => {
return Promise.reject(err);
});
}
If I check the console I can see the request including the token. If I check my network tab in the browser i can see the same request without the token. If I check the console of my api the token is null. Any Ideas?
Edit: If I use postman with the same request and the same token it is working as it shuld
We are having a trouble with our refresh authentication token flow, where if multiple requests are made with the same expired access token at the same time, all of them would make backend to refresh the token and would have different refreshed tokens in their responses. Now when the next request is made, the refresh token of the last response would be used which may or may not be latest.
One solution to this problem is to have UI (which is using axios in vue) send only one request at a time. So if a token is refreshed, the following request would have the latest token.
Hence I am wondering if axios has a default option to disable simultaneous requests as I couldn't find anything online about it. Another solution would be to maintain a queue of requests, where each request is only sent when a response (either success or fail) is received for the previous request(manual option). I understand that this might decrease the performance as it may seem in the UI, but it would also take the load off backend.
One possible solution I found here is to use interceptor:
let isRefreshing = false;
let refreshSubscribers = [];
const instance = axios.create({
baseURL: Config.API_URL,
});
instance.interceptors.response.use(response => {
return response;
}, error => {
const { config, response: { status } } = error;
const originalRequest = config;
if (status === 498) { //or 401
if (!isRefreshing) {
isRefreshing = true;
refreshAccessToken()
.then(newToken => {
isRefreshing = false;
onRrefreshed(newToken);
});
}
const retryOrigReq = new Promise((resolve, reject) => {
subscribeTokenRefresh(token => {
// replace the expired token and retry
originalRequest.headers['Authorization'] = 'Bearer ' + token;
resolve(axios(originalRequest));
});
});
return retryOrigReq;
} else {
return Promise.reject(error);
}
});
subscribeTokenRefresh(cb) {
refreshSubscribers.push(cb);
}
onRrefreshed(token) {
refreshSubscribers.map(cb => cb(token));
}
I am using Apollo (with Graph Cool), redux, and Auth0 in a React-Native app. I am trying to delay the queries and mutations until the header is set.
The idToken is stored in Async Storage, and is therefore a promise. I can't use redux to pass the token, because that would create a circular dependency.
When the user logins in for the first time or the token has expired, the queries are sent before header is set, which means I get the error Error: GraphQL error: Insufficient Permissions
How can I delay the queries until the token is found and added to the header? I have been searching three main solutions:
Add forceFetch: true; This seems to be part of an earlier implementation of the Apollo client. Even if I find the equivalent, the app still fails on the first attempt to fetch.
Reset the store (rehydrate?) upon logging in. This is still asynchronous so I don't see how this could affect the outcome.
Remove all mutations and queries from login itself, but due to the progress of the app, this is not feasible.
Some snippets:
const token = AsyncStorage.getItem('token');
const networkInterface = createNetworkInterface({ uri:XXXX})
//adds the token in the header
networkInterface.use([{
applyMiddleware(req, next) {
if(!req.options.headers) {
req.options.headers = {}
}
if(token) {
token
.then(myToken => {
req.options.headers.authorization = `Bearer ${myToken}`;
})
.catch(err => console.log(err));
}
next(); // middleware so needs to allow the endpoint functions to run;
},
}]);
// create the apollo client;
const client = new ApolloClient({
networkInterface,
dataIdFromObject: o => o.id
});
and
const store = createStore(
combineReducers({
token: tokenReducer,
profile: profileReducer,
path: pathReducer,
apollo: client.reducer(),
}),
{}, // initial state
compose(
applyMiddleware(thunk, client.middleware(), logger),
)
);
I'm not certain this will work without a reproduction app, mostly because I don't have an app of your structure set up, but you're hitting this race condition because you are calling next() outside of your async chain.
Calling next() where it is currently will tell the client to continue on with the request, even if your token isn't set. Instead, let's wait until the token comes back and the header gets set before continuing on.
networkInterface.use([{
applyMiddleware(req, next) {
if(!req.options.headers) {
req.options.headers = {}
}
AsyncStorage.getItem('token')
.then(myToken => {
req.options.headers.authorization = `Bearer ${myToken}`;
})
.then(next) // call next() after authorization header is set.
.catch(err => console.log(err));
}
}]);
Background
I have a restful backend, React + Redux frontend and I'm trying to protect against CSRF and XSS attacks.
The frontend requests a CSRF token from the API. The API response sets the CSRF token in a HttpOnly cookie and also in the response body. The redux reducer saves the token (from the response body) to the redux store.
If I request the token in the main container's componentDidMount(), everything works, but the concern is this is a one shot. Instead, as the requests to the API go through a custom middleware, I would prefer the middleware to request the CSRF token if it doesn't exist locally.
Issue
The flow is as follows (Tested on Chrome 50 and Firefox 47):
CSRF token requested. Token stored in HttpOnly cookie and redux store
Original API call requested with X-CSRF-Token header set. cookie not sent
Receive 403 from API due to missing cookie. API responds with new HttpOnly cookie. The Javascript can't see this cookie, so the redux store is not updated.
Additional API calls requested with X-CSRF-Token header from step 2. and cookie from step 3.
Receive 403 due to mismatched cookie vs X-CSRF-Token
If I add a delay before step 2 with window.setTimeout, the cookie is still not sent, so I don't think it's a race condition with the browser not having enough time to save the cookie?
Action Creator
const login = (credentials) => {
return {
type: AUTH_LOGIN,
payload: {
api: {
method: 'POST',
url: api.v1.auth.login,
data: credentials
}
}
};
};
Middleware
/**
* Ensure the crumb and JWT authentication token are wrapped in all requests to the API.
*/
export default (store) => (next) => (action) => {
if (action.payload && action.payload.api) {
store.dispatch({ type: `${action.type}_${PENDING}` });
return ensureCrumb(store)
.then((crumb) => {
const state = store.getState();
const requestConfig = {
...action.payload.api,
withCredentials: true,
xsrfCookieName: 'crumb',
xsrfHeaderName: 'X-CSRF-Token',
headers: {
'X-CSRF-Token': crumb
}
};
if (state.auth.token) {
requestConfig.headers = { ...requestConfig.headers, Authorization: `Bearer ${state.auth.token}` };
}
return axios(requestConfig);
})
.then((response) => store.dispatch({ type:`${action.type}_${SUCCESS}`, payload: response.data }))
.catch((response) => store.dispatch({ type: `${action.type}_${FAILURE}`, payload: response.data }));
}
return next(action);
};
/**
* Return the crumb if it exists, otherwise requests a crumb
* #param store - The current redux store
* #returns Promise - crumb token
*/
const ensureCrumb = (store) => {
const state = store.getState();
return new Promise((resolve, reject) => {
if (state.crumb.token) {
return resolve(state.crumb.token);
}
store.dispatch({ type: CRUMB_PENDING });
axios.get(api.v1.crumb)
.then((response) => {
store.dispatch({ type: CRUMB_SUCCESS, payload: { token: response.data.crumb } });
window.setTimeout(() => resolve(response.data.crumb), 10000);
// return resolve(response.data.crumb);
})
.catch((error) => {
store.dispatch({ type: CRUMB_FAILURE });
return reject(error);
});
});
};
This was caused because I was creating a new axios client on each request, if I reuse the same axios client for all API requests, the cookie is saved correctly and used in subsequent requests.