React auth with react-query and axios

React auth with react-query and axios

Published

One way of handling authentication with react-query and axios

I am going to try and show you a simple way to handle login and refresh-token state if you have react-query and axios in your arsenal. This approach relies on that your backend returns the refresh-token in a same-site http-only cookie when your users are logging into your application.

We will utilize the axios-hooks package to achieve this, our initial component will look something like this.

function AuthProvider(props: any) {
const accessTokenRef = React.useRef<string>();
const [tokenExpires, setTokenExpires] = React.useState<string>();

...
}

This is how we handle our critical state, now we need to make the user able to login, that can look like this:

const loginRequest = async (username: string, password: string) => {
const { token, ...otherDataYouMightNeed } = await yourBackendLoginRequest(
username,
password
);
return { token, otherDataYouMightNeed };
};

const loginQuery = useMutation(loginRequest, {
onSuccess: (data) => {
// here we rely on the returned data contains the user, the token and its expiration date.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
});

const refreshRequest = async () => {
// it is important that you use axios when fetching the refresh-token, that way we know the cookie
// with the refresh-token is included
const { token, ...otherDataYouMightNeed } = await axios.get(
"/your-refresh-token-endpoint"
);
return { token, otherDataYouMightNeed };
};

// this request should not have to include any logic as we are sending the token value with the cookeis.
const refreshQuery = useMutation(refreshRequest, {
onSuccess: (data) => {
// the refresh-token request should return similiar data as the loginRequest.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
// here we set a refetch-interval to avoid us sending a request without a valid access token.
// you can either hardcode this value or calculate the diff until your token expires.
refetchInterval: 300000,
});

const login = async (username: string, password: string) => {
await loginQuery.mutateAsync(username, password);
// you might want to wrap this in try / catch to handle errors and alert the user
// if the username/password is incorrect.
};

Now we want to bind the access token to our axios config, submitting the credentials with every request to the backend.

useEffect(() => {
// add authorization token to each request
axios.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
config.baseURL = BASE_URL; // base url for your api.
config.headers.authorization = `Bearer ${accessTokenRef.current}`;
// withCredentials should be enabled to submit the cookies with each request.
// this is essential to refresh the access-token.
config.withCredentials = true;
return config;
}
);

axios.interceptors.response.use(
(response) => response,
async (error) => {
return Promise.reject(error);
}
);

// configure axios-hooks to use this instance of axios
configure({ axios });
}, []);

Now, wherever else you import axios around in the application, the config defined above will be used.

The entire component can look something like this:

const loginRequest = async (username: string, password: string) => {
const { token, ...otherDataYouMightNeed } = await yourBackendLoginRequest(
username,
password
);
return { token, otherDataYouMightNeed };
};

const refreshRequest = async () => {
// it is important that you use axios when fetching the refresh-token, that way we know the cookie
// with the refresh-token is included
const { token, ...otherDataYouMightNeed } = await axios.get(
"/your-refresh-token-endpoint"
);
return { token, otherDataYouMightNeed };
};

function AuthProvider(props: any) {
const accessTokenRef = React.useRef<string>();
const [tokenExpires, setTokenExpires] = React.useState<string>();

const loginQuery = useMutation(loginRequest, {
onSuccess: (data) => {
// here we rely on the returned data contains the user, the token and its expiration date.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
});

// this request should not have to include any logic as we are sending the token value with the cookies.
const refreshQuery = useMutation(refreshRequest, {
onSuccess: (data) => {
// the refresh-token request should return similiar data as the loginRequest.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
// here we set a refetch-interval to avoid us sending a request without a valid access token.
// you can either hardcode this value or calculate the diff until your token expires.
refetchInterval: 300000,
});

const login = async (username: string, password: string) => {
await loginQuery.mutateAsync(username, password);
// you might want to wrap this in try / catch to handle errors and alert the user
// if the username/password is incorrect.
};

useEffect(() => {
// add authorization token to each request
axios.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
config.baseURL = BASE_URL;
config.headers.authorization = `Bearer ${accessTokenRef.current}`;
// this is important to include the cookies when we are sending the requests to the backend.
config.withCredentials = true;
return config;
}
);

axios.interceptors.response.use(
(response) => response,
async (error) => {
return Promise.reject(error);
}
);

// configure axios-hooks to use this instance of axios
configure({ axios });
}, []);

const isSuccess = loginQuery.isSuccess || refetchQuery.isSuccess;
const isAuthenticated = isSuccess && !!accessTokenRef.current;
// if you need a user object you can do something like this.
const user = refetchQuery.data.user || loginQuery.data.user;

// example on provider
return (
<AuthContext.Provider
value=
{...props}
></AuthContext.Provider>
);
}

Now you can create a hook where you can use the auth data.

const useAuth = () => {
const context = React.useContext(AuthContext);
if (context === undefined) {
throw new Error("AuthContext must be within AuthProvider");
}

return context;
};

Then you can do something like this around in your application (remember to include AppContext.Provider somewhere in your app).

const { isAuthenitcated, user } = useAuth();

And there you have it, a very simple approach to handle authentication and requests with react-query and axios.

Thanks for reading.