I am creating a react app that is implementing JWT Authentication. I have used createContext() to handle all of the authentication stuff. I will just show my update token method as this is what is causing the issue.
import { createContext, useState, useEffect, useRef, useCallback } from "react";
import jwt_decode from "jwt-decode";
import { useNavigate } from "react-router-dom";
const AuthContext = createContext();
export default AuthContext;
export const AuthProvider = ({ children }) => {
let [user, setUser] = useState(() =>
localStorage.getItem("authTokens")
? jwt_decode(JSON.parse(localStorage.getItem("authTokens")).access)
: null
);
let [accessToken, setAccessToken] = useState(() =>
localStorage.getItem("authTokens")
? JSON.parse(localStorage.getItem("authTokens")).access
: null
);
let [refreshToken, setRefreshToken] = useState(() =>
localStorage.getItem("authTokens")
? JSON.parse(localStorage.getItem("authTokens")).refresh
: null
);
let navigate = useNavigate();
let [loading, setLoading] = useState(true);
let ref = useRef(true);
let updateTokens = useCallback(async () => {
if (refreshToken !== null) {
let response = await fetch("/api/token/refresh/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refresh: refreshToken }),
});
let data = await response.json();
if (response.status === 200) {
localStorage.setItem("authTokens", JSON.stringify(data));
setAccessToken(data.access);
setRefreshToken(data.refresh);
setUser(jwt_decode(data.access));
} else {
setAccessToken(null);
setRefreshToken(null);
setUser(null);
localStorage.removeItem("authTokens");
navigate("/login");
}
}
if (loading) {
setLoading(false);
}
}, [loading, refreshToken, navigate]);
let contextData = {
// Variable
user: user,
accessToken: accessToken,
refreshToken: refreshToken,
loading: loading,
// Functions
loginUser: loginUser,
logoutUser: logoutUser,
};
useEffect(() => {
if (ref.current) {
console.log("hello");
if (loading) {
updateTokens();
}
ref.current = false;
}
let fourMinutes = 1000 * 60 * 4;
let interval = setInterval(() => {
if (refreshToken) {
updateTokens();
console.log("run");
}
}, fourMinutes);
return () => clearInterval(interval);
}, [refreshToken, loading, updateTokens]);
return (
<AuthContext.Provider value={contextData}>{children}</AuthContext.Provider>
);
};
So in the use effect, call updateToken on load, and then every 4 minutes to make sure the access token stays valid (expires every 5 minutes in the back end)
My issue arises when i need to fetch authenticated data on load for a page.
import React, { useEffect, useRef, useState, useContext } from "react";
import AuthContext from "../../context/AuthContext";
const Home = () => {
let { accessToken } = useContext(AuthContext);
let ref = useRef(true);
let getRooms = async () => {
let response = await fetch("api/room/", {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
let data = await response.json();
if (response.status === 200) {
console.log(data);
}
};
useEffect(() => {
if (ref.current) {
ref.current = false;
getRooms();
}
});
return <h1>Home</h1>;
};
export default Home;
Everything works fine, unless a logged in user loads the page and the stored access token is not valid. I am seeing this is because it calls the getRooms function before the authContext has refreshed the token, responding in a 400 bad request. I know the updateToken() method has been called as when i refresh the page, the i get a response 200 from the api with the data as it has now used the new access token.
Im just wondering if there is a way i can make sure the updateToken method in AuthContaxt has finished before fetching any new data on another page??
I followed a tutorial for the authContext, so if you seem to think there is a better way of handling the authentication/refresh of tokens please let me know what i can look into.
2
Answers
I suggest you to use react-cookie module to save your authenticate token to cookie.
https://www.npmjs.com/package/react-cookie
using React query or Tanstack, you can update your original token with refresh token, and check out it.
I see two solutions here
You can add context.isCredentialsLoading prop and place it true by default and load data only after isCredentialsLoading === false
I personally use credential revalidating inside fetch request, if detect that request response is unauthorized then revalidate credentials, logic is kinda like this