I am trying to debug an issue I am having with interceptors in a access token and refresh token reqwuirement. I tried to follow debugging procedures from: Axios interceptors and asynchronous login and also follow the axios interceptor format and procedure from: https://shinework.io/post/handle-oauth2-authentication-with-react-and-flux
However, I can't seem to figure out why my application is not working. I'm new to interceptors and I think it may be an issue with how they are being ejected? I run into an issue during my axios request where I try to run the initial request with an updated access token received from a refresh endpoint. I have this code within my root index.js file
When I debug after the access token has expired, the console.log of 'Rejecting' during the catch block of the initial request call utilizing the new access token renders. Again the purpose is to utilize the refresh token to grab a new sets of access_token and refresh_token, then make the initial request call utilizing the new access_token.
Request and Response Interceptors:
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token');
config.headers.authorization = `Bearer ${token}`;
return config;
},
error => {
return Promise.reject(error);
},
);
axios.interceptors.response.use(
response => {
return response;
},
error => {
const errorMessage = error.message;
const substring = '401';
const errorCheck = errorMessage.includes(substring);
return new Promise((resolve, reject) => {
if (errorCheck) {
onRefreshToken({
initialRequest: error.config,
resolve,
reject,
});
} else {
refreshFailLogout(store);
reject(error);
}
});
},
);
onRefreshToken() and saveTokens()
const onRefreshToken = params => {
let refreshToken = store.getState().auth.refresh_token;
if (refreshToken) {
axios.interceptors.request.eject(InterceptorUtil.getInterceptor());
const dataSet = {
refresh_token: `${refreshToken}`,
};
axios
.post('https://localhost:3469/api/Login/$refresh', dataSet)
.then(response => {
saveTokens(response.data);
// Replay request
axios(params.initialRequest)
.then(response => {
params.resolve(response);
store.dispatch({ type: AUTHENTICATED, payload: response.data });
})
.catch(response => {
console.log('Rejecting')
params.reject(response);
})
.catch(() => {
refreshFailLogout();
});
});
}
};
const saveTokens = response => {
const {
access_token,
refresh_token,
scope,
id_token,
token_type,
} = response;
// ...local storage save of variables
let token = localStorage.getItem('access_token');
let interceptor = axios.interceptors.request.use(config => {
config.headers.authorization = `Bearer ${token}`;
return config;
});
InterceptorUtil.setInterceptor(interceptor);
};
InterceptorUtil Class
class InterceptorUtil {
constuctor() {
this.interceptor = null;
}
setInterceptor(interceptor) {
this.interceptor = interceptor;
}
getInterceptor() {
return this.interceptor;
}
}
export default new InterceptorUtil();
Related
I am trying to update access token using refresh tokens. I am using axios interceptors to call the /refreshendpoint which returns a new access token. I am setting the token and refresh token in local storage. however, when the token is expired, the getAll method is called before the response interceptor. the token does get updated in the local storage after I call the /refresh endpoint in the response interceptor, but in the getAll method, the token that's set in the header is still the previous expired token. When I refresh the page though, the new token is set in the header and it works as expected. when I see the server console, it shows the /patients endpoint is called before the /refresh endpoint. I am calling the getAll method when the component mounts.
axios interceptor code:
import { getFromLS } from "./localStorage";
const baseURL = "http://localhost:4200/api"
const instance = axios.create({
baseURL,
});
instance.interceptors.request.use((request: AxiosRequestConfig) => {
axios.defaults.headers.common["Authorization"] = "";
delete axios.defaults.headers.common["Authorization"];
if (getFromLS("token")) {
if(request.headers) {
request.headers.Authorization = getFromLS("token");
}
}
return request;
});
instance.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;
if (
error.response.status === 401 &&
originalRequest.url === `${baseURL}/refresh`
) {
return Promise.reject(error);
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
instance
.post("/refresh", {
body: {
id: Number(getFromLS("user")),
refreshToken: getFromLS("refreshToken"),
},
})
.then((response) => {
const newToken = response.data.data.newAccessToken;
localStorage.setItem("token", newToken);
instance.defaults.headers.common[
"Authorization"
] = `Bearer ${newToken}`;
return instance(originalRequest);
})
}
return Promise.reject(error);
}
);
export default instance;
the getAll method which uses the above axios instance:
get: (url: string) => instance.get<IPatientResponse>(url).then(responseBody)
getAll: (): Promise<IPatientResponse[]> => get("/patients"),
I am building a jwt token refresh logic (refresh the authentication token when it expires) with axios interceptors. The refresh part works well : axios intercepts the error, refreshes the token, and retries the request (and successfully gets an answer from the server).
However, the page that made the request that failed because of the expired token still catches the error. I feel like axios still returns the error to the function that made the call instead of just returning the retried request, but idk how.
Here is the code in my axios.js file :
import { boot } from "quasar/wrappers";
import axios from "axios";
import * as storage from "../helpers/storage";
import store from "../store/index.js";
import router from "../router/index.js";
const api = axios.create({
baseURL: process.env.API_URL,
crossdomain: true,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
api.interceptors.request.use(
function (config) {
if (config.url !== "/register") {
const accessToken = storage.getAccessToken();
if (accessToken) {
config.headers.Authorization = "Bearer " + accessToken;
}
}
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
api.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
if (error.response.data.message === "Expired JWT Token") {
const originalRequest = error.config;
api
.post("/token/refresh", { refresh_token: storage.getRefreshToken() })
.then(({ data }) => {
if (data !== undefined) {
storage.setTokens(data.token, data.refresh_token);
}
originalRequest.headers = { Authorization: `Bearer ${data.token}` };
return new Promise(() => {
axios.request(originalRequest).then((response) => {
return response;
});
});
})
.catch((error) => {
console.error(error);
});
} else if (error.response.data.message === "Invalid JWT Token") {
console.log("error");
store()
.dispatch("auth/logout")
.then(() => {
router().push({
name: "register-login",
query: { error: "invalid_token" },
});
router().go(0);
store().dispatch("setLoading", false);
});
} else {
return Promise.reject(error);
}
}
);
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
});
export { axios, api };
And here is an example of a request I do :
export function sendTags(context, payload) {
return new Promise((resolve, reject) => {
api
.post("/spot/addTags", payload)
.then(({ data }) => {
resolve(data);
})
.catch((error) => {
reject(error.response.data);
});
});
Any idea of what could be going wrong ?
You didn't return a success result in the error function of response interceptor.
api.interceptors.response.use(
function (response) {
return response;
},
function (error) {
if (error.response.data.message === "Expired JWT Token") {
// You didn't return here!
// change to:
return api.post()
.than(() => {
// resolve the final result here
return axios.request(originalRequest)
})
}
}
)
Recently updated SWR - now for some reason my data is not fetching properly.
const { data: expressionsData, error: expressionsError } = useSWRImmutable(
[`dashboard/expression/get-expression-analytics?startTime=${startDate}&endTime=${endDate}`, startDate, endDate],
apiRequest
);
Using this fetching,
import firebase from "./firebase";
export async function apiRequest(path, method = "GET", data) {
const accessToken = firebase.auth().currentUser
? await firebase.auth().currentUser.getIdToken()
: undefined;
//this is a workaround due to the backend responses not being built for this util.
if (path == "dashboard/get-settings") {
return fetch(`/api/${path}`, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: data ? JSON.stringify(data) : undefined,
})
.then((response) => response.json())
.then((response) => {
if (response.error === "error") {
throw new CustomError(response.code, response.messages);
} else {
return response;
}
});
}
return fetch(`/api/${path}`, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: data ? JSON.stringify(data) : undefined,
})
.then((response) => response.json())
.then((response) => {
console.log("error", response);
if (response.status === "error") {
// Automatically signout user if accessToken is no longer valid
if (response.code === "auth/invalid-user-token") {
firebase.auth().signOut();
}
throw new CustomError(response.code, response.message);
} else {
return response.data;
}
});
}
// Create an Error with custom message and code
export function CustomError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
// Check if a indexDb database exists
export function indexedDbdatabaseExists(dbname, callback) {
const req = window.indexedDB.open(dbname);
let existed = true;
req.onsuccess = function () {
req.result.close();
if (!existed) window.indexedDB.deleteDatabase(dbname);
callback(existed);
};
req.onupgradeneeded = function () {
existed = false;
callback(existed);
};
}
Now I'm looking at this StackOverflow thread,
useSWR doesn't work with async fetcher function
And thinking I'll just remake the fetcher to be without Async. I'm just wondering why this has stopped working though in general, and if I can just keep my existing codebase.
The error is a 400 message, it only happens with this expressions API call which takes longer to load due to the amount of data I think,
xxxx/dashboard/expression/get-expression-analytics?startTime=1648183720488&endTime=1650865720488 400 (Bad Request)
with error log
These calls are working fine, they have substantly less data though.
const { data: overall, error: psychometricError } = useSWRImmutable(
`dashboard/psychometric/get-psychometric-home?starttime=infinite`,
apiRequest
);
const { data: sentimentData, error: sentimentError } = useSWRImmutable(
[`dashboard/sentiment/get-sentiment-timefilter?startTime=${startDate}&endTime=${endDate}`, startDate, endDate],
fetchSentiment
);
Made an update to the fetch call to be more readable and specifically about the URL pathway.
import firebase from './firebase';
// Create an Error with custom message and code
export function CustomError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
export async function expressionsRequest(path, method = 'GET') {
const accessToken = firebase.auth().currentUser
? await firebase.auth().currentUser.getIdToken()
: undefined;
return fetch(`/api/${path}`, {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
.then((response) => {
if (!response.ok) {
throw `Server error: [${response.status}] [${response.statusText}] [${response.url}]`;
}
return response.json();
})
.then((receivedJson) => {
if (receivedJson.status === 'error') {
// Automatically signout user if accessToken is no longer valid
if (receivedJson.code === 'auth/invalid-user-token') {
firebase.auth().signOut();
}
throw new CustomError(receivedJson.code, receivedJson.message);
} else {
return receivedJson.data;
}
})
.catch((err) => {
console.debug('Error in fetch', err);
throw err;
});
}
Additionally, this is what the lambda function (using next API folder) looks like,
const requireAuth = require('../../_require-auth');
const { db } = require('../../_sql');
export default requireAuth(async (req, res) => {
const { uid: id } = req.user;
const startTime = Math.round(req.query.startTime * 0.001);
const endTime = Math.round(req.query.endTime * 0.001);
const parameters = [id, startTime, endTime];
//sql injection definitely possible here, need to work out better method of dealing with this.
const sqlText = `SELECT a,b,c,d,e,f,g,h,i FROM tablename WHERE a=$1 AND i BETWEEN $2 AND $3;`;
try {
const { rows } = await db.query(sqlText, parameters);
return res.status(200).json({
code: 0,
data: rows,
});
} catch (error) {
return res.status(200).json({
code: 0,
message: 'Error occurred in getting tablename',
error,
});
}
});
using postman with the same query, i.e.,
curl --location --request GET 'http://localhost:3000/api/dashboard/expression/get-expression-analytics?startTime=1648387240382&endTime=1651069240382' \
--header 'Authorization: Bearer xxxx' \
--data-raw ''
Successfully returns a response with data attached.
Based on your first code blocks, the startDate value is getting passed into the fetcher as method, and the endDate value is getting passed into the fetcher as data. This is based on the useSWR docs about passing in an array for the key argument: https://swr.vercel.app/docs/arguments#multiple-arguments
If the code you provided is correct, I'd assume the 400 is coming from trying to pass in a random value for the method option for fetch.
This should be fixed by only passing the API endpoint path into useSWR instead of an array:
const { data: expressionsData, error: expressionsError } = useSWRImmutable(
`dashboard/expression/get-expression-analytics?startTime=${startDate}&endTime=${endDate}`,
apiRequest
);
I am trying to request a new access token using a refresh token if the current access token is expired. I have the following code setup for this
axios.interceptors.response.use(undefined, function(error) {
if (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
axios.post("auth/refresh", {
refreshToken: store.state.refreshToken
}).then(res => {
store.dispatch('setToken', res.data.token)
store.dispatch('setRefToken', res.data.refreshToken)
error.config.headers[
"Authorization"
] = `Bearer ${res.data.token}`;
})
} else {
return Promise.reject(error);
}
}
})
This seems to work and get the new access token from the server.
The problem now is this, I need to refresh the page for the new Auth Headers to be set and that is not ideal since a logged-in user may be performing an action and may not know to refresh the page after the token expires.
How can I achieve this and ensure the user does not experience any glitch?
axios.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
const originalConfig = err.config;
if (originalConfig.url !== "/login" && err.response) {
if (err.response.status === 401 && !originalConfig._retry) {
originalConfig._retry = true;
try {
const rs = await axios.post("auth/refresh", {
refreshToken: store.state.refreshToken
});
const {
token,
refreshToken
} = rs.data;
store.dispatch('setToken', token)
store.dispatch('setRefToken', refreshToken)
err.config.headers[
"Authorization"
] = `Bearer ${token}`;
return new Promise((resolve, reject) => {
axios.request(originalConfig).then(response => {
resolve(response);
}).catch((err) => {
reject(err);
})
});
} catch (_error) {
return Promise.reject(_error);
}
}
}
return Promise.reject(err);
}
);
Update: I did a lot of digging and read through a lot of articles to see what was wrong with my initial code and approach.
And I found a fix that suits my use case perfectly. It's a bit messy but works.
In my VUE components, I use this async method to fetch data from API:
Components:
methods: {
async fetch() {
// console.log("##### WAIT ####");
const { data } = await staffRepository.getItems(this.teamId)
// console.log("##### END WAIT ####");
this.staffs = data
},
},
As you can see I use a custom repository to have a single axios code, this repository is imported in my previous component.
staffRepository:
export default {
getItems(nationId) {
return Repository.get(`page/${nationId}`)
},
}
And finally the main repository having the axios code:
Repository:
import axios from 'axios/index'
const baseDomain = 'https://my end point'
const baseURL = `${baseDomain}`
...
const headers = {
'X-CSRF-TOKEN': token,
// 'Access-Control-Allow-Origin': '*', // IF you ADD it add 'allowedHeaders' to ai server config/cors.php
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtoken}`,
}
export default axios.create({
baseURL,
withCredentials: withCredentials,
headers: headers,
})
This code works very nice when the jwtoken is a valid and NOT EXIPRED token.
The problem is when the token is expired or not found and my laravel 5.8 API returns the status code 401 (or other).
GET https://api.endpoint 401 (Unauthorized)
A good solution could catch the status code in staffRepository, the one having the get method.
MySolution: (not working)
getItems(nationId) {
return Repository.get(`page/${nationId}`)
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error.response.status) // <-- it works!
})
},
This could be nice because in error case the error in console is 401
But I can't use this solution because I have 2 nested promises: this one and the async fetch() into the component.
How can I fix it still using my repository environment?
I would suggest using the returned promise in your component, to make things more explicit:
methods: {
fetch() {
let data = null
staffRepository
.getItems(this.teamId)
.then(data => {
// do something with data
this.staffs = data
})
.catch(e => {
// do something with error, or tell the user
})
},
},
Edit - this will work perfectly fine, as your method in Repository will return a promise by default if you are using axios.
Try this: API code, where HTTP is an axios instance
export const get = (path: string): Promise<any> => {
return new Promise((resolve, reject) => {
HTTP.get(`${path}`)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(handleError(error));
});
});
};
// ***** Handle errors *****/
export function handleError(error) {
if (error.response) {
const status = error.response.status;
switch (status) {
case 400:
// do something
break;
case 401:
// do something, maybe log user out
break;
case 403:
break;
case 500:
// server error...
break;
default:
// handle normal errors here
}
}
return error; // Return the error message, or whatever you want to your components/vue files
}
The best practice solution is to use axios's interceptors:
import axios from "axios";
import Cookies from "js-cookie";
export default (options = {}) => {
let client = options.client || axios.create({ baseURL: process.env.baseUrl });
let token = options.token || Cookies.get("token");
let refreshToken = options.refreshToken || Cookies.get("refreshToken");
let refreshRequest = null;
client.interceptors.request.use(
config => {
if (!token) {
return config;
}
const newConfig = {
headers: {},
...config
};
newConfig.headers.Authorization = `Bearer ${token}`;
return newConfig;
},
e => Promise.reject(e)
);
client.interceptors.response.use(
r => r,
async error => {
if (
!refreshToken ||
error.response.status !== 401 ||
error.config.retry
) {
throw error;
}
if (!refreshRequest) {
refreshRequest = client.post("/auth/refresh", {
refreshToken
});
}
const { data } = await refreshRequest;
const { token: _token, refreshToken: _refreshToken } = data.content;
token = _token;
Cookies.set("token", token);
refreshRequest = _refreshToken;
Cookies.set("refreshToken", _refreshToken);
const newRequest = {
...error.config,
retry: true
};
return client(newRequest);
}
);
return client;
};
Take a look at client.interceptors.response.use. Also you should have a refreshToken. We are intercepting 401 response and sending post request to refresh our token, then waiting for a new fresh token and resending our previous request. It's very elegant and tested solution that fits my company needs, and probably will fit your needs too.
To send request use:
import api from './api'
async function me() {
try {
const res = await api().get('/auth/me')
// api().post('/auth/login', body) <--- POST
if (res.status === 200) { alert('success') }
} catch(e) {
// do whatever you want with the error
}
}
Refresh token: The refresh token is used to generate a new access
token. Typically, if the access token has an expiration date, once it
expires, the user would have to authenticate again to obtain an access
token. With refresh token, this step can be skipped and with a request
to the API get a new access token that allows the user to continue
accessing the application resources.