NextJS / React - how to avoid multiple requests to server? - reactjs

I'm writing a little side project in which I have a question about how to design my frontend authorization flow.
Stack is: Next.js / Express / MDB
Here's the issue with the authorization: In my app there are pages that should be only served to instructors.
When the app mounts I have a call to my backend checking for a stored cookie and authorize the user. The returned user is stored in a Context.
authContext.js
useEffect(() => {
checkUserLoggedIn();
}, []);
...
const checkUserLoggedIn = async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_CLIENT}/user`);
const data = await res.json();
if (res.status === 200) {
setUser(data.user);
} else {
setUser(null);
}
};
That works perfectly fine.
But now comes the part where I struggle.
The protected pages are wrapped by a component that checks again if the user is a) authenticated and b) authorized.
wrapInstructor.js
const CheckInstructor = ({ token, children }) => {
const [ok, setOk] = useState(false);
const [login, setLogin] = useState(null);
useEffect(async () => {
try {
const user = await fetch(
`${process.env.NEXT_PUBLIC_APIURL}/me`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const { data, error } = await user.json();
if (error) {
toast.error("Auf diese Seite können nur eingeloggte User zugreifen");
setLogin(false);
}
if (data && data.role.includes("INSTRUCTOR")) {
setOk(true);
setLogin(true);
}
} catch (error) {
toast.error(error);
setLogin(false);
}
}, []);
return !ok ? (
<>{login === false ? <NotLoggedIn /> : <Spinner />}</>
) : (
<>{children}</>
);
};
export default CheckInstructor;
Now here comes the problem:
When a user mounts the app on a path that is protected the app fires off two authentication requests simultaniously.
The first one gets a valid response (the request from context). The second one from the wrapper gets a 304 status code which indicates that the response has not changed since the last request.
But since the wrapper expects a valid response the content of the protected pages will not show.
How can I cope with this problem? (NOTE: I'm in development mode, tested from localhost)
My ideas were:
The wrapper component does not make another call to the server - the user in the context is the single source of truth and the wrapper only checks the already stored user --> Question: Is this a safe practice? Can the store (React Context or Redux) be manipulated by the user which would make my app unsafe?
In the wrapper component also show the children components if the status code is 304 AND a user is already stored in the context
Write another endpoint in the backend so both requests are sent to different routes (I would concider this bad practice because DRY)
I'm a bit lost here - can you help me to clear my thoughts?
Thanks a lot!

There's no need to visit the same endpoint twice to check if a user is authorized to visit a certain page. What you store in auth context would be enough. Just make sure to update the value whenever a role or permissions change.
Regarding your security concern, you shouldn't consider any client app safe. Even if you add an actual endpoint call on every protected page, there's still a way to call the endpoints directly(curl, postman, you name it).
The problem should be solved by introducing authorization checks on every protected API route. This way you would never worry about corrupted clients at all.

Related

access Nextjs httponly cookie

I'm working on a Nextjs app and I'm using Laravel api for auth and other things.
So I was searching about the best way to store the token that i will get when I sign a user in or sign him up from that Laravel external api and as I found storing it via httponly cookie is the best way and that's what I did using nextjs api routes to store it there.
I create 3 api routes in /api directory for loging the user in and up and out.
but now I'm facing an issue which is how to send this token on each request that i'm sending on client side to that api.
for now I'm using getServerSideProps to get the token and then send it by props to the needed component but this is solution is redundant and not handy at all.
I'm using getServerSideProps just to get it on any page that need to communicate with backend.
so is there any way to forward the token on each request without the need to get the token from server side and send it to client side?
or do u think there is a better way to store the token in somewhere else?
If you have a httpOnly cookie and you don't want to expose it to the client-side, I don't suggest to pass it in getServerSideProps as prop to the component. This will expose your token and will have the risk of scripts accessing the cookie. If the cookie is HttpOnly, it means it cannot be accessed through the client-side script.
Instead, you can create a NextJS api and access your token there. Then call that api in your app.
This is an example of how to access the cookie in nextjs api:
// src > pages > api > endpoint.js
export default async function (req, res) {
// assuming there is a token in your cookie
const token = req.headers.cookie.token;
// Attach the token to the headers of each request
const fetchWithToken = (url, options = {}) => {
options.headers = options.headers || {};
options.headers.Authorization = `Bearer ${token}`;
return fetch(url, options);
};
// Your API logic here
// ...
}
Now in your component in client-side, you can easily the new endpoint in /api/endpoint/ instead of calling the API with the token. In this method, you will never expose the token to the browser:
import { useState, useEffect } from 'react';
const MyComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const res = await fetch('/api/endpoint');
const data = await res.json();
setData(data);
};
fetchData();
}, []);
// Your component logic here
// ...
};
export default MyComponent;

Making authFetch from react-token-auth doesn't use access token

I'm building a webapp using react and flask. I'm currently implementing user login features using react-token-auth and flask_praetorian. I've already created my back-end functions that handle logging in and can successfully return a token. However, I am now having issues with making an authenticated request.
My flask function
#app_login.route('/get_username')
#flask_praetorian.auth_required
def protected():
response = jsonify({'username': flask_praetorian.current_user().username})
return response
and on react
const fetchUsername = () => { authFetch(`http://${configData.LOCAL_SERVER}:${configData.WEBAPP_PORT}/get_username`).then(response => {
return response.json()
}).then(response => {
console.log(response)
})
}
I'm using the default createAuthProvider as shown on the react-token-auth project page
export const { useAuth, authFetch, login, logout } = createAuthProvider({
getAccessToken: session => session.accessToken,
storage: localStorage,
onUpdateToken: token =>
fetch(`http://${configData.LOCAL_SERVER}:${configData.WEBAPP_PORT}/app_login/refresh`, {
method: 'POST',
body: token.refreshToken,
}).then(r => r.json()),
});
Whenever I make a request, I get a 401 (UNAUTHORIZED) and react returns the error 'authFetch' was called without access token. Probably storage has no session or session were expired
I've checked my local storage and I can see that the key is there:
Storage {persist:root: '{"player":"{\\"value\\":{\\"name\\":\\"\\",\\"access_toke…_persist":"{\\"version\\":-1,\\"rehydrated\\":true}"}', REACT_TOKEN_AUTH_KEY: '"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2…5OX0.rTBCD7YPD8wrB95v1j9oazNLusKOPErI5jed_XWXDhU"', length: 2}
I'm really trying to avoid using technologies like Redux so any assistance regarding this specific setup would be great.

Next.js production: CANNOT READ DATA FROM FIRESTORE

I have a next.js application in which I have a real-time chat feature.
A get() request is supposed to be sent to firestore to check if chat between user A and user B already exists. If it does not already exist, a new chat between A and B is created
Problem:
The get() request to firestore always returns an empty value. So a new chat room between user A and user B is always created.
Since a new chat room is created successfully, I can write to firestore but cannot read data.
the code:
import { useEffect, useState } from "react";
import { dbs } from "../utils/firebase";
function Test() {
const [chats, setChats] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await dbs
.collection("chats")
.where("users", "array-contains", "some#email")
.get();
setChats(response);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);
console.log("chat is", chats);
return <div></div>;
}
export default Test;
What I have attempted and discovered:
I deployed an entirely new next.js app (let's call it app B) and tried to get data from the same firestore project with the same code above and it works. So my firestore configuration is fine, the code is fine, and there's nothing wrong with the deployment server.
Since there's nothing wrong with firestore or the deployment server or the code, I think something is wrong with the next.js app itself and the dependencies surrounding it. I don't know why it works in app B and not in app A.
Note, in App A:
Everything works fine in development mode, I can get the data.
I'm also using mongoDB and other third-party APIs in this app and I can successfully read and write data to these APIs. Only the get() request to firestore is not working. which made me think the problem must be with my firestore config but app B proves that's not the case.
I'm using google OAuth with mongoDB and next-auth to authenticate my users in app A. In app B I'm only using google OAuth without the DB.
I could show the code for both app A and app B but they are exactly the same. The code under the _app.js file is the same, the way I structure it is also the same. In both app A and app B, I called the get() request to firestore from a Test.js component in a test.js page but the get() request only returns value in app B and always returns null in app A.
So basically the only difference is app A is an actual app with plenty of dependencies, libraries, and files. app B is just a test app.
My question now is:
Could the read operation to firestore be affected by other dependencies or npm libraries in the project? or could it be affected by using another DB to get the auth session context?
Why is the write operation successful but not the read?
Why does it work in dev mode but not in prod mode? is there any difference between retrieving data from firestore in dev mode vs prod mode?
As explained yesterday, you aren't paying close enough attention to the intermediate states properly and it is likely that NextJS is tripping up because of it.
On the first render of your current code, chats is an empty array. Even so, once the promise fulfills, you update chats to a QuerySnapshot object, not an array.
import { useEffect, useState } from "react";
import { dbs } from "../utils/firebase";
function Test() {
const currentUserEmail = "some#email"; // get from wherever
const [chatsInfo, setChatsInfo] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
let unsubscribed = false;
const fetchData = async () => {
return dbs
.collection("chats")
.where("users", "array-contains", currentUserEmail)
.get();
};
fetchData()
.then(() => {
if (unsubscribed) return; // do nothing
setChatsInfo({
status: "loaded",
data: response.docs, // QueryDocumentSnapshot[]
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // do nothing
setChatsInfo({
status: "error",
data: null,
error: err
});
});
};
// return a unsubcribe callback that makes sure setChatsInfo
// isn't called when the component is unmounted or is out of date ​
​return () => unsubscribed = true;
​ }, [currentUserEmail]); // rerun if user email changes
​
​ const { status, data: chats, error } = chatsInfo;
console.log(JSON.parse(JSON.stringify({ status, chats, error }))); // crude object copy
switch (status) {
case "loading":
return (<div>Loading...</div>);
case "error":
return (
<div class="error">
Failed to load data: {error.code || error.message}
</div>
);
}
// if here, status is "loaded"
if (chats.length === 0) {
// You could also show a "Message Someone" call-to-action button here
return (
<div class="error">
No chat groups found.
</div>
);
}
// stamp out list of chats
return (<>
{chats.map((chatDoc) => {
return (
<div key={chatDoc.id}>
{JSON.stringify(chatDoc.data())}
</div>
);
})}
</>);
}
export default Test;
Notes:
A decent chunk of the above code can be eliminated by using an implementation of useAsyncEffect like #react-hook/async and use-async-effect. These will handle the intermediate states for you like loading, improper authentication, unmounting before finishing, and other errors (which are all not covered in the above snippet). This thread contains more details on this topic.
I highly recommend not using email addresses in their raw form for user IDs. With the current structure of your database, anyone can come along and rip all the emails out and start spamming them.
Each user should have some private identifier that doesn't reveal sensitive information about that user. This could be a Firebase Authentication User ID, the user's email address hashed using md5 (which also allows you to use Gravatar for user profile pictures) or some other ID like a username. Once you have such a user ID, you can use the approach outlined in this thread for handling messages between users.

How to wait for auth state to load on the client before fetching data

Recently I stumbled across the useAuth and useRequireAuth hooks: https://usehooks.com/useRequireAuth/. They are incredibly useful when it comes to client-side authentication. However, what's the correct way for waiting until auth data is available to fetch some other data? I've come up with the following code:
const Page = () => {
// makes sure user is authenticated but asynchronously, redirects if not authenticated, short screen-flash
useRequireAuth()
// actual user object in state, will be updated when firebase auth state changes
const user = useStoreState((state) => state.user.user);
if (!user) {
return <div>Loading</div>
}
useEffect(() => {
if (user) {
fetchSomeDataThatNeedsAuth();
}
}, [user]);
return (
<h1>Username is: {user.name}</h1>
)
}
Is this a "good" way to do it or can this be improved somehow? It feels very verbose and needs to be repeated for every component that needs auth.
This looks fine to me. The thing you could improve is that your useRequireAuth() could return the user, but that's up to you.
Additionally, you probably should check if user is defined before rendering user.name.

React Relay Modern redirecting to another page when receiving 401 error on network environment

I´m using JWT authentication inside my ReactJS RelayJS network environment. All the token retrieval and processing in server and client are fine. I´m using react router v4 for routing.
My problem is when I receive a Unauthorized message from server (status code 401). This happens if the user points to an application page after the token has expired, ie. What I need to do is to redirect to login page. This is the code I wish I could have:
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
const SERVER = 'http://localhost:3000/graphql';
const source = new RecordSource();
const store = new Store(source);
function fetchQuery(operation, variables, cacheConfig, uploadables) {
const token = localStorage.getItem('jwtToken');
return fetch(SERVER, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
Accept: 'application/json',
'Content-type': 'application/json'
},
body: JSON.stringify({
query: operation.text, // GraphQL text from input
variables
})
})
.then(response => {
// If not authorized, then move to default route
if (response.status === 401)
this.props.history.push('/login') <<=== THIS IS NOT POSSIBLE AS THERE IS NO this.history.push CONTEXT AT THIS POINT
else return response.json();
})
.catch(error => {
throw new Error(
'(environment): Error while fetching server data. Error: ' + error
);
});
}
const network = Network.create(fetchQuery);
const handlerProvider = null;
const environment = new Environment({
handlerProvider, // Can omit.
network,
store
});
export default environment;
Naturally calling this.props.history.push is not possible as the network environment is not a ReactJS component and therefore has no properties associated.
I´ve tried to throw an error at this point, like:
if (response.status === 401)
throw new Error('Unauthorized');
but I saw the error on the browser console, and this cannot be treated properly in the code.
All I wanna do is to redirect to login page in case of 401 error received, but I can´t find a proper way of doing it.
I am not using relay but a render prop. I experienced kind of the same issue. I was able to solve it using the window object.
if (response.statusText === "Unauthorized") {
window.location = `${window.location.protocol}//${window.location.host}/login`;
} else {
return response.json();
}
You can go with useEnvironment custom hook.
export const useEnvironment = () => {
const history = useHistory(); // <--- Any hook using context works here
const fetchQuery = (operation, variables) => {
return fetch(".../graphql", {...})
.then(response => {
//...
// history.push('/login');
//...
})
.catch(...);
};
return new Environment({
network: Network.create(fetchQuery),
store: new Store(new RecordSource())
});
};
// ... later in the code
const environment = useEnvironment();
Or you can create HOC or render-prop component if you are using class-components.
btw: this way you can also avoid usage of the localStorage which is slowing down performance.

Resources