Checking is user is authenticated with JWT, express and React on backend - reactjs

I have a problem, I am using Express + React with Typescript. My question is about authentication. I would like to send the token to the backend and respond if the user is logged in or not, on every request. Is there a better way than this included in Frontend, because I need to do the same logic for every private route. I know that I can make checking authentication functions reusable, but I still need to put the logic for rendering pages conditionally. I have one more idea to solve that with wrapper but because of the asynchronous nature of setting states in react I get isAuth false and I am navigated to the "/" every time.
Backend:
export const isAuth = async (req: Request, res: Response) => {
const token = req.get('Authorization')?.split(' ')[1];
if (token) {
try {
const decodedToken = jwt.verify(token, `${process.env.SECRET_KEY}`);
if (decodedToken) res.status(200).json({ isAuth: true });
} catch (e) {
res.status(403).json({ isAuth: false });
}
} else res.status(403).json({ isAuth: false });
};
Frontend
const AddProductPage = () => {
const [isAuth, setIsAuth] = useState<boolean>(false);
useEffect(() => {
const isAuth = async () => {
const response = await fetch(`${config.backendDomain}/auth/is-auth`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + localStorage.getItem('token'),
},
});
const data = await response.json();
setIsAuth(data.isAuth);
};
isAuth();
}, []);
return isAuth ? <AddProduct /> : <Navigate to="/" />;
};
Wrapper approach
interface wrapperProps {
children: React.ReactNode;
}
const IsAuth: React.FC<wrapperProps> = (props) => {
const [isAuth, setIsAuth] = useState(false);
useEffect(() => {
const isAuth = async () => {
const response = await fetch(`${config.backendDomain}/auth/is-auth`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + localStorage.getItem('token'),
},
});
const data = await response.json();
setIsAuth(data.isAuth);
};
isAuth();
}, []);
console.log('ISAUTH', isAuth);
return isAuth ? <div>{props.children}</div> : <Navigate to="/" />;
};

my solution which seemed to me the most reusable was the wrapper idea which I implemented like this, using the Loading state which allowed me to wait for the asynchronous operation. Do not hesitate to share your opinions/ideas. :)
const IsAuth = (props: wrapperProps) => {
const [isAuth, setIsAuth] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const isAuth = async () => {
const response = await fetch(`${config.backendDomain}/auth/is-auth`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + localStorage.getItem('token'),
},
});
const data = await response.json();
setIsAuth(data.isAuth);
setIsLoading(false);
};
isAuth();
}, []);
useEffect(() => {
setIsLoaded(!isLoading && isAuth);
}, [isAuth, isLoading]);
if (isLoaded) return <div>{props.children}</div>;
if (!isLoading && !isAuth) return <Navigate to="/" />;
return <div>Loading</div>;
};
export default IsAuth;

If you are using Express.js as your back-end web framework I recommend using express-jwt middleware which checks the Authorization header on every request and basically replaces your isAuth function you wrote.
As a side note, you should return a 401 Unauthorized status code and not a 403 Forbidden. express-jwt will do this for you.
Then, on the front-end, you would perform requests with the token as you did with fetch. But would need to check for 401 errors (and potentially other errors) to decide what to render.
With this approach you would not need to make 2 network calls to the server each time you want communicate, which can cause not only poor performance but also unexpected bugs.

Related

Not able to implement data from one api used to get data from another

I am making a meme sharing app. In that app there are total 2 apis of getting memes.
One for memes by all the users another is only for individual user.
In second api I am able to get the data as the user id is from 3rd api.
from here i get the id of each individual.
function UserProfile({memeid}) {
const token = localStorage.getItem("token");
const [response, setResponse] = useState({});
const [id, setId] = useState('')
const userData = async() => {
await axios
.get("http://localhost:8081/userInfo/me", {
headers: { Authorization: `Bearer ${token}` },
Accept: "application/json",
"Content-Type": "application/json",
})
.then((res) => {
setResponse(res.data)
setId(res.data.id)
memeid = id
})
.catch((err)=>{
console.log(err)
})
}
console.log(id)
useEffect(()=>{
userData()
},[])
Now I want this to be used in in another api. for that is have written this code.
function MemeById({id}) {
const [response, setResponse] = useState([])
const token = localStorage.getItem("token");
// const id = "632a119672ba0e4324b18c7d"
const memes = async () => {
await axios
.get("http://localhost:8081/memes/" + id, {
headers: { Authorization: `Bearer ${token}` },
Accept: "application/json",
"Content-Type": "application/json",
})
.then((res) => {
const data = res.data;
setResponse(res.data)
console.log(data);
})
.catch((err) => {
alert(err);
console.log(err);
});
};
useEffect(()=>{
memes()
},[])
I am calling these two at User
function User() {
let id;
return (
<div>
<UserProfile memeid={id}/>
<MemeById id = {id} />
</div>
)
}
I am getting the error for this.
How to solve this error
You're making a big mistake. I think you should learn more about state and props in react.
Problem :
In your User component, you're creating a variable and passing that variable into two other component. You're trying to update the value of props from UserProfile and expecting that updated value in MemeById which is not going to work.
Solution :
function User() {
const [memeId, setMemeId] = useState(null);
return (
<div>
<UserProfile updateId={(newId) => setMemeId(newId)}/>
<MemeById memeId = {memeId} />
</div>
)
}
And in your UserProfile component
function UserProfile({updateId}) {
...
const userData = async() => {
...
// memeid = id
updateId(res.data.id)
...
}
In you MemeById component:
function MemeById({memeId}) {
...
// use memeId here
...
}

Why is UseEffect not launching the entire async function?

I am using Auth0 on a react application for authentication. After authenticating, I am trying to save the data from authentication to the database (I'm using node.js and mongoose/mongodb). I created a function that makes a post request and also accesses a token with an Auth0 function. When I execute the function in useEffect, the token gets created, as I can see it in the network tab on the Chrome debugger. But, the response api call never gets made. Do you know why this is? Here is my code:
const [data, setData] = useState({
message: '',
error: null,
loading: true
});
const [userData, setUserData] = useState(null);
const serverUrl = 'http://localhost:5000';
const loginOrCreateUser = useCallback(async () => {
try {
const token = await getAccessTokenSilently();
const response = await fetch(`${serverUrl}/users/login`, {
headers: {
Authorization: `Bearer ${token}`,
},
method: "POST",
body: {
name: user.name,
email: user.email,
_id: user.sub
}
});
const responseData = await response.json();
setUserData(responseData);
console.log(userData);
} catch (error) {
setData({...data, error: error.error});
}
});
useEffect(()=> {
loginOrCreateUser();
}, []);
return (
isAuthenticated && (
<div>
<img src={user.picture} alt={user.name} />
<h2>{user.name}</h2>
<Button onClick={callApi}>Call API</Button>
<p>{JSON.stringify(data.message)}</p>
</div>
)
);
}

How to create a global 401 unauthroized handler using React + ReactQuery + Axios stack?

So I architected frontend in the way which encapsulates every API operation tied to a single resource inside custom hook like this:
export default function useSubjects() {
const queryClient: QueryClient = useQueryClient();
const token: string | null = useStore((state) => state.user.token);
const { yearCourseId } = useParams<{ yearCourseId: string }>();
const getSubjects = async () => {
const response = await axios.get(`yearCourses/${yearCourseId}/subjects`, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
};
const postSubject = async (subject: SubjectType) => {
const response = await axios.post(`yearCourses/${yearCourseId}/subjects`, subject, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
};
const query = useQuery(SUBJECTS_QUERY_KEY, getSubjects);
const postMutation = useMutation(postSubject, {
onSuccess: (subject: SubjectType) => {
queryClient.setQueryData(SUBJECTS_QUERY_KEY, (old: any) => [...old, subject]);
},
});
return { query, postMutation };
}
Now what is the way to globally handle 401 unauthorized? I would like to navigate user to /login on every unauthorized request. Note that I have more hooks like this tied to other resources.
use the onError callback. You can also do this globally as a callback on the queryCache
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: error => {
// check for 401 and redirect here
}
})
})

ReactJS how can I fetch data only once a state has been updated, and only once [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 9 months ago.
I am new to React and am having a difficult time figuring out how I can wait for the state to have a specific (not null) update before fetching data. I am using firebase JWT and am passing the token into the headers but with my current code it runs and passed the value of null. Is there a nifty hook trick to ensure that my fetchData function only runs once and that it only runs after the token value is set?
I tried setting the state as const [token, setToken] = useState(auth.currentUser.getIdToken()); but it appears to return a promise into the header and not the token (guessing its because its async). Thanks!
import React, { useState, useEffect } from 'react';
import { auth } from '../../firebase-config';
const RecordEntry = (props) => {
const [token, setToken] = useState();
const [isLoading, setIsLoading] = useState(false);
var mydata =
{
entry_id = props.entry_id
}
//should only call this once
const fetchData = async () => {
const current_token = auth.currentUser.getIdToken();
setToken(current_token);
//need to yield here to verify token is set and not null - this is where I am stuck
fetch('https://mysite/api/recordEntry' , {
method: 'POST',
headers: new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
}),
body: JSON.stringify(mydata)
})
.then((response) => response.json())
.then((data) => {
setIsLoading(false);
})
.catch((error) => {
setIsLoading(false);
console.log(error);
});
};
//passing empty array so the effect only runs once
useEffect(() => {
fetchData();
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1> Entry Recorded </h1>
</div>
);
};
export default RecordEntry;
Try this solution
const [didFetch,setDidFetch] = useState(false)
useEffect(() => {
if(!didFetch){
setDidFetch(true)
fetchData();
}
}, []);
"Thanks for the response, I attempted this solution but the token is still not updated. The header shows it's a promise object, instead of the expected token string. The token is still awaiting to update essentially. I need a method that pauses the data fetch until the token is filled."
So try this:
const [token, setToken] = useState(null);
And
useEffect(() => {
if (token != null) fetchData();
}, [token]);
Using #awolf's suggestion of await for current_token and then bass that as the auth bearer instead of the version updating to state. Worked perrfectly. Here is the final solution:
import React, { useState, useEffect } from 'react';
import { auth } from '../../firebase-config';
const RecordEntry = (props) => {
const [token, setToken] = useState();
const [isLoading, setIsLoading] = useState(false);
var mydata =
{
entry_id = props.entry_id
}
//should only call this once
const fetchData = async () => {
const current_token = await auth.currentUser.getIdToken();
setToken(current_token);
//need to yield here to verify token is set and not null - this is where I am stuck
fetch('https://mysite/api/recordEntry' , {
method: 'POST',
headers: new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${current_token}`,
}),
body: JSON.stringify(mydata)
})
.then((response) => response.json())
.then((data) => {
setIsLoading(false);
})
.catch((error) => {
setIsLoading(false);
console.log(error);
});
};
//passing empty array so the effect only runs once
useEffect(() => {
fetchData();
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1> Entry Recorded </h1>
</div>
);
};
export default RecordEntry;

Async react hook

I would like create async React hook in React-Native for get AsyncStorage data before run my fetch.
Example :
const useCallApi = async url => {
const [instance, token] = await Promise.all([
AsyncStorage.getItem('instance'),
AsyncStorage.getItem('token')
]);
const data = useFetch(`${instance}/api/v1/${url}`, {
headers: {
Authorization: `Bearer ${token}`
}
});
return data;
};
export default useCallApi;
But this hook return an error Unhandled promise rejection. I think the problem is await before useFetch hook, but how i can fix this case ?
If anyone can help me :)
Thank you community,
Why not use AsyncStorage in another way? Only after initialization is complete, you can access AsyncStorage synchronously from anywhere.Through react-native-easy-app, you can operate AsyncStorage like this
import { XStorage } from 'react-native-easy-app';
import { AsyncStorage } from 'react-native';
export const RNStorage = {
token: undefined,
isShow: undefined,
userInfo: undefined
};
const initCallback = () => {
// From now on, you can write or read the variables in RNStorage synchronously
// equal to [console.log(await AsyncStorage.getItem('isShow'))]
console.log(RNStorage.isShow);
// equal to [ await AsyncStorage.setItem('token',TOKEN1343DN23IDD3PJ2DBF3==') ]
RNStorage.token = 'TOKEN1343DN23IDD3PJ2DBF3==';
// equal to [ await AsyncStorage.setItem('userInfo',JSON.stringify({ name:'rufeng', age:30})) ]
RNStorage.userInfo = {name: 'rufeng', age: 30};
};
XStorage.initStorage(RNStorage, AsyncStorage, initCallback);
Maybe adding await before AsyncStorage helps you:
const useCallApi = async url => {
const [instance, token] = await Promise.all([
await AsyncStorage.getItem('instance'),
await AsyncStorage.getItem('token')
]);
const data = useFetch(`${instance}/api/v1/${url}`, {
headers: {
Authorization: `Bearer ${token}`
}
});
return data;
};
export default useCallApi;
const useCallApi = async url => {
let instance = null;
let token = null;
Promise.all([
AsyncStorage.getItem('instance'),
AsyncStorage.getItem('token')
]).then(d=>{
instance = d[0];
token = d[1];
}).catch(e=>throw e);
const data = useFetch(`${instance}/api/v1/${url}`, {
headers: {
Authorization: `Bearer ${token}`
}
});
return data;
};
export default useCallApi;
I think a promise needs then when it resolve and a catch for error catching
You should not implement the hooks like that, it will cause call fetch many times whenever the component which uses this hook re-render.
Try this instead:
const useCallApi = url => {
const [data, setData] = useState(null)
useEffect(() =>{
const fetchData = async () =>{
const [instance, token] = await Promise.all([
AsyncStorage.getItem('instance'),
AsyncStorage.getItem('token')
]);
// I assume that your useFetch is a hook that retruen a Promise of data
const fetchedData = await useFetch(`${instance}/api/v1/${url}`, {
headers: {
Authorization: `Bearer ${token}`
}
});
setData(fetchedData)
}
fetchData()
},[url])
return data;
};

Resources