I'm having an issue where I do not find a security-first and maintenable answer anywhere.
Imagine a dashboard doing multiple queries at the same time, how do you handle refresh_tokens in a clean and stadard way?
The stack is (even if the stack doesn't matter here):
Backend - Laravel with a JWT token authentification Frontend - Vue JS with axios for the API calls
Endpoints:
JWT refresh workflow
axios.get('/auth/login').then(res => setTokenAndUser(res))
// ... just some pseudo code
userDidAction() {
axios.get('/statistics').then(res => handleThis(res.data));
axios.get('/other-statistics').then(res => handleThat(res.data));
axios.get('/event-more-statistics').then(res => handleThisAgain(res.data));
axios.get('/final-statistics').then(res => handleThatAgain(res.data));
}
// ...
This is a very common scenario on SPAs and SaaS apps. Having multiple asynchronous API calls is not an edge case.
What are my options here ?
My current idea is to make the access_token last 3 days and the refresh_token last a month with the following workflow :
This makes the refresh_token travel less on the network and makes parallel fails impossible since we change tokens only when the frontend loads initially and therefore, tokens would live for at least 12h before failing.
Despite this solution working, I'm looking for a more secure / standard way, any clues?
For the refresh token, we will simply generate a UID and store it in an object in memory along with the associated user username. It would be normal to save it in a database with the user's information and the creation and expiration date (if we want it to be valid for a limited period of time).
So the answer is, Yes you can (and probably should) wait until your access token expires, and then refresh it.
When to use Refresh Tokens? The main purpose of using a refresh token is to considerably shorten the life of an access token. The refresh token can then later be used to authenticate the user as and when required by the application without running into problems such as cookies being blocked, etc.
So here is the situation I had in an application and the way I solved it:
I saved the token data in a session storage and update it with refresh token API response each time
I had three get request in one page, and I wanted this behavior that when token expires ONLY one of them get to call the refresh Token API and the others have to wait for the response, when the refresh token promise is resolved all three of them should repeat the failed request with updated token data
So here is the vuex setup:
// here is the state to check if there is a refresh token request proccessing or not
export const state = () => ({
isRefreshing: false,
});
// mutation to update the state
export const mutations = {
SET_IS_REFRESHING(state, isRefreshing) {
state.isRefreshing = isRefreshing;
},
};
// action to call the mutation with a false or true payload
export const actions = {
setIsRefreshing({ commit }, isRefreshing) {
commit('SET_IS_REFRESHING', isRefreshing);
},
};
and here is the axios setup:
import { url } from '@/utils/generals';
// adding axios instance as a plugin to nuxt app (nothing to concern about!)
export default function ({ $axios, store, redirect }, inject) {
// creating axios instance
const api = $axios.create({
baseURL: url,
});
// setting the authorization header from the data that is saved in session storage with axios request interceptor
api.onRequest((req) => {
if (sessionStorage.getItem('user'))
req.headers.authorization = `bearer ${
JSON.parse(sessionStorage.getItem('user')).accessToken
}`;
});
// using axios response interceptor to handle the 401 error
api.onResponseError((err) => {
// function that redirects the user to the login page if the refresh token request fails
const redirectToLogin = function () {
// some code here
};
if (err.response.status === 401) {
// failed API call config
const config = err.config;
// checks the store state, if there isn't any refresh token proccessing attempts to get new token and retry the failed request
if (!store.state.refreshToken.isRefreshing) {
return new Promise((resolve, reject) => {
// updates the state in store so other failed API with 401 error doesnt get to call the refresh token request
store.dispatch('refreshToken/setIsRefreshing', true);
let refreshToken = JSON.parse(sessionStorage.getItem('user'))
.refreshToken;
// refresh token request
api
.post('token/refreshToken', {
refreshToken,
})
.then((res) => {
if (res.data.success) {
// update the session storage with new token data
sessionStorage.setItem(
'user',
JSON.stringify(res.data.customResult)
);
// retry the failed request
resolve(api(config));
} else {
// rediredt the user to login if refresh token fails
redirectToLogin();
}
})
.catch(() => {
// rediredt the user to login if refresh token fails
redirectToLogin();
})
.finally(() => {
// updates the store state to indicate the there is no current refresh token request and/or the refresh token request is done and there is updated data in session storage
store.dispatch('refreshToken/setIsRefreshing', false);
});
});
} else {
// if there is a current refresh token request, it waits for that to finish and use the updated token data to retry the API call so there will be no Additional refresh token request
return new Promise((resolve, reject) => {
// in a 100ms time interval checks the store state
const intervalId = setInterval(() => {
// if the state indicates that there is no refresh token request anymore, it clears the time interval and retries the failed API call with updated token data
if (!store.state.refreshToken.isRefreshing) {
clearInterval(intervalId);
resolve(api(config));
}
}, 100);
});
}
}
});
// injects the axios instance to nuxt context object (nothing to concern about!)
inject('api', api);
}
and here is the situation as shown in network tab:
as you can see here there are three failed request with 401 error, then there is one refreshToken request, after that all failed requests get called again with updated token data
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With