Why doesn't this update of an axios interceptor work? - reactjs

I have an axios client, defined in a provider context and accessed throughout my app with a useAxios hook.
The provider uses an access token, which gets updated every few minutes using a token refresh. This is definitely updating correctly.
I use an interceptor to add the token to the request headers. But of course, I want the latest token. So, each time the token updates, I change the interceptor to use the new one. I do this in a useEffect which only fires if the token changes, and I have verified that this effect is firing correctly.
But, any use of the axios client subsequent to a token refresh still uses the original interceptors - the ones that were defined when the app was first loaded. Consequently, after the original token times out, all my requests are unauthenticated even though I've added an interceptor with an up to date token, because teh original interceptor is being used...
THE QUESTION
How do I correctly update this interceptor so the latest one is used on every query?
THE CODE
import React, { useEffect } from 'react'
import axios from 'axios'
import { useAuth } from './useAuth'
import { apiErrors } from 'errors'
export const AxiosContext = React.createContext(undefined)
const AxiosProvider = ({ children }) => {
const { accessToken } = useAuth()
const axiosClient = React.useMemo(() => {
return axios.create({
headers: {
'Content-Type': 'application/json',
},
})
}, [])
// Attach request interceptor to add the auth headers
useEffect(() => {
console.log(
'VERIFIED TO WORK - THIS SHOWS PERIODICALLY ON TOKEN UPDATE, WITH THE NEW TOKEN',
accessToken
)
axiosClient.interceptors.request.use((request) => {
if (accessToken) {
request.headers.Authorization = `JWT ${accessToken}`
}
return request
})
}, [axiosClient, accessToken])
return (
<AxiosContext.Provider value={axiosClient}>
{children}
</AxiosContext.Provider>
)
}
// A hook for accessing this authenticated axios instance
function useAxios() {
return React.useContext(AxiosContext)
}
export { AxiosProvider, useAxios }

Related

Should I store a Axios instance configured with interceptors that use a state value in a state or ref or useMemo?

Similiar to Should we use axios inside both components and store?
I have a state called authorization which contains the Bearer token value that would be used in Axios calls. The state is available in a context and accessible using the useContext hook.
I create the AxiosInstance where I add a interceptors.request.use to add the Authorization header.
What I've done so far was useMemo with the authorization value a a dependency. But since Axios operation is asynchronous it seems that I may get the wrong axios instance.
I did a bit of refactoring using useRef and I still had a bit of an issue.
What I then did was implement the Observer pattern and send a notification to the component that provides the Axios client that the authorization header was changed and update the ref. However, again there's still some cases where the old client is being invoked.
What I am wondering is should I store it in useState or is there a fundamental problem with the approach of storing the Axios client and instead should I just bite the bullet and create a new axios client per request which takes the authorization header that's presently in the state?
The way I typically do it is to save the authentication information in a React context or to redux, and create axios instances as needed to access the authorization token.
Maybe something like:
const getBearerToken() => { ... [your implementation to retrieve from context here] ... };
const webserviceRequest = (url) => () => axios.create({
baseURL: url,
... [AxiosRequestConfig options here] ...
headers: {
Authorization: `Bearer ${getBearerToken()}`,
...
},
});
Then, you can define your webservice requests by invoking this function, e.g.:
const sampleGetRequest = () => webserviceRequest(SAMPLE_URL)().get('');
const samplePostRequest = (postData) => webserviceRequest(SAMPLE_URL)().post('', postData);
These return a Promise<AxiosResponse> which you can call as normal, e.g.
sampleGetRequest().then((response) => { ... }).catch((error) => { ... })
and so on.
The key point is that the webserviceRequest returns a function which creates an asynchronous webservice request with the current authorization token. You don't ever save the webserviceRequest function because that authorization token can become stale.
With the dependency on React context, I'd avoid using interceptors for this (I'd avoid using Axios all together but that's just my opinion).
Instead, try creating a custom hook
import axios from "axios";
import { useContext } from "react";
const api = axios.create({ /* baseURL, etc */ });
const useApi = () => {
const authorization = useContext(AuthContext); // guessing
if (authorization) {
api.defaults.headers.common.Authorization = `Bearer ${authorization}`;
} else {
delete api.defaults.headers.common.Authorization;
}
return api;
};

axios interceptor not resending failed request

I am creating this React dashboard app. I'm currently trying to set up an axios interceptor to refresh my access token when I send a request with an expired access token to my backend.
I have a refresh async action in my slice that makes a get request to my backend with my refresh token cookie. That is being called from my axiosInstance response interceptor.
After that returns, I set the access token in my store
Then try to rerun the original failed request
return axiosInstance(originalRequest)
The problem is the return statement to rerun the request is not working. when I view my devtools network tab I only see the preflight, failed post request, then refresh request.
There should be another retry of the first post request after the refresh
Here is my axios code
-Creating my instance and interceptors
import axios from 'axios';
import { store } from '../../../app/store';
import { refresh } from '../../auth/authSlice';
const axiosInstance = axios.create({
timeout:5000
})
axiosInstance.defaults.baseURL = process.env.REACT_APP_BASE_SCRAPE_URL
axiosInstance.defaults.withCredentials = true;
axiosInstance.interceptors.request.use(function(config){
const state = store.getState();
const token = state.auth.accessToken
config.headers.Authorization = `Bearer ${token}`
return config
})
axiosInstance.interceptors.response.use(resp => resp,async error =>{
const orginalRequest = error.config
console.log(error.response);
if(error.response.data === "Invalid Access Token"){
store.dispatch(refresh({}))
return axiosInstance(orginalRequest)
}
})
export default axiosInstance
Using the axios Instance (Request that needs to rerun after initial failed request)
import axiosInstance from "./protectedScrapeAxios"
export const start = async (pageName, postCount) =>{
const resp = await axiosInstance.post('/start', {pageName: pageName, postCount: postCount})
return resp.data
}

Axios - New Instance overrides Global Defaults

quick question on implementation of multiple axios instances.
I call my own API as needed inside my mobile app. Certain API endpoints are protected by checking that the token issued by Google Signin or Facebook Signin is authenticated and matches the expected user. Other endpoints do not require a token and actually require the frontend to not send a token at all to proceed down the proper logic path on the backend.
I set the global axios instance when I fetch the user token on frontend. I am hoping that all axios calls include the token by default. To achieve this, I set the default Authorization header as such:
import axios from 'axios'
...
axios.defaults.headers.common.Authorization = `Bearer ${fbUserInfo.accessToken}`
This code runs on the initial page of the app that is loaded, so everytime I use axios somewhere else in the app, the token is now included. It works as expected until I proceed to the following step.
In order to create some recurring API calls where a token should not be included, I create a "tokenless" instance inside one of my redux action creators.
const axiosTokenlessInstance = axios.create()
...
axiosTokenlessInstance.defaults.headers.common.Authorization = false
This does indeed override the default axios settings when I call it directly, however, I've realized that it also has overridden my default settings when I just call axios directly again.
I expected the behavior to be that I could still call axios.post or axios.get elsewhere and it would still include the token. Was I mistaken and do I have to create a "tokened" instance? I'd prefer not to do this as I'd have to go through and replaced tokened instance explicitly everywhere rather than just using axios default. Thanks!
Try creating a file called setAuthToken.js that handles the auth instance separately. This way any time you need to use the auth, you can just call this function ( I would even suggest limiting to calling this auth only once and having the token saved in the users localStorage):
import axios from 'axios';
const setAuthToken = token => {
if(token){
axios.defaults.headers.common['x-auth-token'] = token;
}
else{
delete axios.defaults.headers.common['x-auth-token'];
}
}
export default setAuthToken;
Now let's say you have a backend function call that needs auth verification, you can do it like so:
import setAuthToken from "../utils/setAuthToken";
export const loginUser = (token) => async (dispatch) => {
if (localStorage.token) {
setAuthToken(localStorage.token);
}
try {
const res = await axios.get("/api/auth");
dispatch({
type: USER_LOGGED,
payload: res.data,
});
} catch (err) {
dispatch({
type: AUTH_ERROR,
});
}
};
Specifying the blank header Authorization inside axios.create() fixes the issue and doesn't override the global axios headers.
So this works as expected:
const axiosTokenlessInstance = axios.create({
baseURL: Config.API_HOST,
headers: { Authorization: '' },
})
While this overrides global axios settings, not just affecting the specified instance:
const axiosTokenlessInstance = axios.create()
axiosTokenlessInstance.defaults.headers.common.Authorization = false

How to modify axios instance after exported it in ReactJS?

I am using:
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
to set header after user make login in application, but when refresh the page this configuration is removed.
I would like to set this configuration for all requests from axios, when user make login.
I got do that setting this configuration manually, putting this line of code before to export axios instance.
Now, I need to set this configuration when user make login. How can I do that?
You're probably going to want to write a middleware module to get/set the token in localStorage and apply it to your Axios instance. In the past when I used Axios, I typically did it like this:
import axios from 'axios';
import { API_URL } from '../constants/api';
const API = axios.create({
baseURL: `${API_URL}`,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
API.interceptors.request.use(
config => {
const token = sessionStorage.getItem('jwt');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
delete API.defaults.headers.common.Authorization;
}
return config;
},
error => Promise.reject(error)
);
export default API;
You'll need to create functions to get/set the JWT in localStorage, but if you do that, this should work for you. This will fetch the JWT from localStorage before making each request, so it won't break even if the page is refreshed as long as the user has a valid JWT in localStorage.
I have the same issue as Hiatt described:
refresh the page will invalidate my previous default config
while I don't feel like reading storage before every request
so what I did is check before request and read if necessary (eg: default were reset due to page reload
// request interceptor function
function check_before_request(config) {
if (! config.headers.common['Authorization']) {
const token = Cookies.get('Authorization')
if (! token){
removeCookies()
location.href = `/login?redirect=${encodeURIComponent(location.pathname)}`
}
else {
setHeaderAuth(token, config)
}
return config
}
else return config
}
// also can be used in login page but without the second param
function setHeaderAuth(token, config) {
Cookies.set('Authorization', token)
axios.defaults.headers.common['Authorization'] = token;
if (config){
config.headers['Authorization'] = token
}
}

Best practice/method to refresh token with AWS Cognito and AXIOS in ReactJS

I am doing the below in my App.JS but it is not refreshing the token in the other components. Can some one suggest what would be the best way to check if the token is valid or refresh it from all the components before the AXIOS call is made. If you could provide a link to any example it would be great. Thank you.
import { Auth } from "aws-amplify";
import setAuthorizationToken from './components/utility/SetAuthorizationToken';
async componentWillMount() {
try {
console.log("Im am in componentWillMount() in App.js");
await Auth.currentSession().then(data => {
this.setAuthStatus(true);
}).catch(err => {
this.setAuthStatus(false)
});
await Auth.currentAuthenticatedUser().then(user => {
this.setUser(user);
setAuthorizationToken(user.signInUserSession.idToken.jwtToken);
});
} catch(error) {
if (error !== 'No current user') {
console.log(error);
}
}
this.setState({ isAuthenticating: false });
}
SetAuthorizationToken.js
import axios from 'axios';
export default function SetAuthorizationToken(token) {
if(token){
axios.defaults.headers.common['Authorization'] = token;
}else{
delete axios.defaults.headers.common['Authorization'];
}
}
H, welcome to SO.
You do not have to track the JWT token or user or refresh it by yourself with cognito.
For the axios call just use await Auth.currentSession() before the axios call and inject the token directly from the callback into your axios call. currentSession will only return a valid token and will try to refresh it, if it is expeired.
That way, you can rely on AWS to always provide you with a valid token without tracking it yourself.
You also do not have to save the user since you can access him anywhere you need it with the currentAuthenticatedUser call.
Hope this helps. Happy coding.

Resources