Axios interceptor expiration time - javascript

Below I will post my AXIOS code. What happens first when I start the application (Vue js) is that it opens login page and when I enter username/password I set token and refresh token in local storage. That works fine and I can make other api calls which require token. The problem happens when it expires (in 3 minutes after login), it goes into infinite loop. Also the thing is I don't know how to check expiration time of refresh token because I can't decode it with this function I wrote.
Access token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIyIiwianRpIjoiMTg4YjQ0NDg2MzdmMmJhYWRkMDE1MmU5OWRhNGIwMWYxMzgxMzVjY2Q5YjA2NmRlM2M4YjFkZjk4ODE1ZGZmNGZhOGE2ODQ2YWI4ZjY1YjkiLCJpYXQiOjE2MjE1MzcxOTAuNjg5MjgzLCJuYmYiOjE2MjE1MzcxOTAuNjg5Mjg2LCJleHAiOjE2MjE1MzczNzAuNjc3NDM3LCJzdWIiOiIyIiwic2NvcGVzIjpbXX0.U5lNHetMq6vEnUKwxlJ9sa9lU6ahj-lDlxjWFdaTuXaGCcmx8zb917OSKkZa1g8PA3NArC6nMVbWfbD44DXLF3I6UFFXAYNncuH8kAngIh-XyRhUgr3MDOR04dCb02Khchs30QnbznHFvox1wtTXLEIT2wzdGI0_GGQot3ZFvxBfukRVt64uqC7GrVxcpoZXV2LXY7LxkZXoEd88QFcjfWWw_RC1fyU7gNaGxF4xml5CyJGZOcM1S-1QlBsXE-HE5qeJPZilxOJLHvxSYo-HFTbl7u0WNlryyCAxJqoeMHIqmHrEmZX261IdMFdQ7sl9YP-rXtg5hY_SDVoaE-KjHThltKvPkV_XeWxWQ3KqCDqm7UMZyxkWzEMglE4Ym8hvNsgUIlZMVeKCuYkQ2Vri-X2whttaVwM4-pJPbAqJURYu2WRDWgBbIWWkXkw4GLUFTDIllOmIBESUjba_L3x2dHrce3PpBOBw8dYDPttdqch6t_J7vBsRUu8-DcHDzxnVu6vBYmQA-TAlI9yN7gOgn_gMDMq6FhitKuQ9KghACJmTjqB-_BbxAI3pWwAuPeAas7uB9ugzpScKPPZtThoI08wQ8pT7Xz8JvZTEharzUHcldu2rIlUCif6l-rtszIQNYcCfWFMBVP9HFRSgCcEtgl3L5SPfQGW0Ytc2P_ED4HE
Refresh token:
def502009a8ebae0e2d2d18b541daa39725ed826115ce9612db43e399b3edc7fe7d08950e5972b8c13faa8846962fb4a027a95a5cdefcdb526051644a1031c909ee6ad1cbc421aa12d0a096728dff99e8c72aef8e7e527824287274cee8d702d20e7468985d5d648c990df99990c283b490bb33d97cf2ecfaf176e6ecd2259db183d95d7bdd664600319d6af36e463e777e01cdd364cdbf146d10ea9a58a1ba5b01adb98ac7ffb27ebfd10cb62f79d1bcb7bf13c3adc1fe70f9de554bc98258ac8071d46a1cc51812140fae06291868016e97b39bb31b8a749a4ed2daa78f53e66256351d3aada01f5bb7ffcc5d4f8494cb0116b9816e92ba614e78dff8730b7e81b22049b73a69956a55daeec3b53c4a87f34280af1451cf67e81f804346fa1f121788af98becedb896991c8349e87eace91b73019381cc8550160742e3141ea7ef3eb0a71333496489c3e43c47fb18c076d2a9950ffe6dc6138bea1f7cd8cbf3
They are different, when I decode access token I get an object which has this key "exp":1621533539.9695 which is expiration time, but not for refresh token.
Any idea?
export const API = axios.create({
baseURL: BASE_URL,
withCredentials: false,
headers: {
Accept: "*/*",
"Access-Control-Allow-Origin": "*",
},
});
// CHECK IF TOKEN IS EXPIRED (DECODE TOKEN)
function isTokenExpired(token) {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
const data = JSON.parse(jsonPayload);
const expirationDate = data.exp;
const currentDate = new Date().getTime() / 1000;
return currentDate >= expirationDate;
}
API.interceptors.request.use(
async (config) => {
let token = localStorage.getItem("accessToken");
let refreshToken = localStorage.getItem("refreshToken");
//if refresh token is expired we logout and return early
// if (refreshToken) {
// const isExpired = isTokenExpired(refreshToken);
// if (isExpired) {
// localStorage.setItem("accessToken", "");
// localStorage.setItem("refreshToken", "");
// router.push({ path: "/login" });
// }
// return config;
// }
// if token is expired we refresh the token
if (token) {
const isExpired = isTokenExpired(token);
if (isExpired) {
const data = await API.post("/token", {
grant_type: "refresh_token",
refresh_token: refreshToken,
});
if (data) {
token = data.access_token;
localStorage.setItem("accessToken", data.data.access_token);
localStorage.setItem("refreshToken", data.data.refresh_token);
}
}
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
//CHECK AFTER REQUEST IF RESPONSE IS 401
API.interceptors.response.use(
(response) => response,
async (error) => {
let refreshToken = localStorage.getItem("refreshToken");
if (error.response?.status && error.response?.status === 401) {
const data = await API.post("/token", {
grant_type: "refresh_token",
refresh_token: refreshToken,
});
if (data) {
localStorage.setItem("accessToken", data.data.access_token);
localStorage.setItem("refreshToken", data.data.refresh_token);
error.config.headers.Authorization = "Bearer " + data.access_token;
return API.request(error.config);
}
}
throw error.response;
}
);

The refresh token does't come with and expiration date.
You need to try getting a new token with it. If it is invalid, your API will tell you, then you need to send your user back to login page.
I think you know the refresh token flow, but I'll leave it here anyway:
Check if you have a token and a Refresh Token in your localStorage.
Once you have them, check if the token is expired.
if it is expired, you need to send the refresh token to your api.
The API will give you another token and refreshToken, store them and continue making requests.
If the API reject the refreshToken, send your user to login page.
I have a complete example of refreshToken and reattempt request if you need.
Everytime your token expires you need to do the refresh token process.

Related

Axios Interceptor doesn't set Token to the Authorization Header

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.

Different headers used in Axios patch

I spent an hour looking in the Chrome console and I cannot see where this bug comes from.
I am finishing an update of OAuth implementation in my Vue app.
The story begins when socialLink.js finds out that a new user must be created. Vue component Vue-authentication depends on the presence of access_token in a response so I return some dummy text:
return api.sendResponse(res, { email, name, socialId, access_token: 'abcd' });
The library stores this value in localStorage:
After a redirect, the SignUp.vue is rendered and I complete the form. The first communication with the server is a Vuex call to create a new user:
response = await this.$store.dispatch('CREATE_USER_PROFILE', payload);
Which returns a real short lived JWT token:
const token = auth.createToken(userId, nickname, new Date(), null, false, '1m');
return api.sendCreated(res, api.createResponse(token));
Which I store in the Vue page afterwards:
const { data } = response;
const token = data.data;
if (token === undefined) {
this.error = this.$t('sign-up.something-went-wrong');
return false;
}
I checked that the token contains what the server returned:
Request URL: https://beta.mezinamiridici.cz/api/v1/users
Request Method: POST
Status Code: 201 Created
{"success":true,"data":"eyJhbGciOiJIUzI1NiIs...Tl8JFw2HZ3VMXJk"}
Then I call another Vuex method and pass the current JWT token:
await this.$store.dispatch('UPDATE_USER_PROFILE', {
I checked in the Vuex devtools that there really is the correct JWT token. I then pass it further to api.js.
Here I create an Axios configuration holding an Authorization header:
function getAuthHeader(context, jwt = undefined, upload) {
const config = { headers: { } };
if (jwt || (context && context.rootState.users.userToken)) {
config.headers.Authorization = `bearer ${jwt || context.rootState.users.userToken}`;
}
Again, I checked that the correct JWT token is used there.
Finally, I pass all data to Axios:
function patch(endpoint, url, body, context, jwt) {
const headers = getAuthHeader(context, jwt);
console.log(headers);
if (endpoint === 'BFF') {
return axios.patch(`${VUE_APP_BFF_ENDPOINT}${url}`, body, headers);
} else {
return axios.patch(`${VUE_APP_API_ENDPOINT}${url}`, body, headers);
}
}
Which I log and can confirm the correct JWT is still there:
bearer eyJhbGciOiJIUzI1N....8JFw2HZ3VMXJk
There is nothing that could change the header now to abcd, but, the 'Network' tab shows it:
And the server fails with a parse error.
Has anybody got an idea why Axios uses the Authorization header with a different value than I pass it?
Ok, mystery solved. vue-authenticate is the reason, because, it creates Axios interceptors and handles the Authorization header itself.
vue-authenticate.common.js:
var defaultOptions = {
bindRequestInterceptor: function ($auth) {
var tokenHeader = $auth.options.tokenHeader;
$auth.$http.interceptors.request.use(function (config) {
if ($auth.isAuthenticated()) {
config.headers[tokenHeader] = [
$auth.options.tokenType, $auth.getToken()
].join(' ');
} else {
delete config.headers[tokenHeader];
}
return config
});
},
My code is more complex and it supports internal accounts with email/password so this code is breaking mine. The interceptor must be present and be a function, so the solution was:
Vue.use(VueAuthenticate, {
tokenName: 'jwt',
baseUrl: process.env.VUE_APP_API_ENDPOINT,
storageType: 'localStorage',
bindRequestInterceptor() {},
bindResponseInterceptor() {},
providers: {
facebook: {
clientId: process.env.VUE_APP_FACEBOOK_CLIENT_ID,
redirectUri: process.env.VUE_APP_FACEBOOK_REDIRECT_URI,
},

When token is refreshed subsequent API calls pass stale token

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)
}
)

extract the expiration datetime from jsonwebtoken

To invalidate a token it's as far as I know the best way to store the token and it's expiration datetime to the database. To validate it, you simply have to select it from the database and if it exists, you know it was invalidated. Further you can remove every expired token by it's expiration datetime from the database.
So I created a middleware that extracts the token from the authorization headers and it should attach the token and the expiration datetime to the request object. The datetime is required for the signOut route to invalidate the token.
async use(req: any, res: Response, next: NextFunction) {
try {
const headers: IncomingHttpHeaders = req.headers;
const authorization: string = headers.authorization;
const bearerToken: string[] = authorization.split(' ');
const token: string = bearerToken[1];
if (await this.authenticationsRepository.findByEncodedToken(token)) { // invalidated token?
throw new Error(); // jump to catch
}
req.tokenPayload = verifyToken(token); // calls jwt.verify with secret
next();
} catch (error) {
throw new UnauthorizedException();
}
}
But how can I extract the exp attribute from the token to calculate the expiration date time?
In order to get expiration date you need to decode the jsonwebtoken and access it's exp key, kind of like this:
let token = jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: '1h' });
var decoded = jwt.decode(token, { complete: true });
console.log(decoded.payload.exp);
In your case you can do it like this I think:
req.expirationTime = jwt.decode(token, { complete: true }).payload.exp;

Refresh JWT once across mulitple browser windows/tabs

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" });
});
}
}

Categories

Resources