Handling Pre-Fetched Dynamic Client Routes in Gatsby / #ReachRouter - javascript

I have this scenario.
A user types in /company/<company-id> in the address bar.
Since the app is totally separate from the backend, it needs to prefetch the companies.
Normal user flow is /login -> /company/. I handle this case pretty well and just navigate to /company/<whatever id is first in the prefetch> with no problems.
But what if you load WITH the id? I have solution but I think I have a feeling that I'm misunderstanding something in routing.
You may assume that my prefetching works and the code snippet below will only trigger if companyState.success is true. Like i said, it is working.
I handled this manually, by
// pretty sure i can handle this with regex better to capture other cases
// but that's beside the point for the scope of this question
const urlId = +location.pathname.replace("/company/", "")
const checkCompany = !!companyState.data.find(d => d.id === urlId)
if(checkCompany){
company.set(urlId)
}
else{
navigate("/404")
}
I have hooks in place where in if company.set(<company:id>) does update, it will pre-fetch everything else needed for the view. And, company is a custom context hook so that it's present everywhere in my application.
Is there a better way in handling this? It seems hack-y to manually check the path name.
You can assume that my gatsby_node.js has the right definitions to allow the client side routing.
Here's my routing definitions: (this is what i put in the pages folder)
const DashboardPage = () => (
<ProtectedRoute>
<Router>
<Company path="/company/*" />
</Router>
</ProtectedRoute>
)
Finally in the components folder,
const Company = ({location}) => (
<Router>
<Main path="/:companyId">
<Summary path="/" />
.... other dashboard routes
</Main>
</Router>
)

You have to assume that client-side code can always be changed by a malicious actor. Ultimately, you have to make sure on the backend that a user can only request ressources he is supposed to see.
Your solution for the client-side seems fine to me. I don't see another way than checking the URL path manually and then redirecting.
By logging in, your user needs to be assigned to a cryptographically safe cookie or token (such as JSON web tokens) so you can always be sure of their identity. Everytime a company id is routed to, your frontend needs to send the user identity to your backend. Only there you can be safe from code manipulations. Your backend needs to check if the user can look at this page.
If the user can: your backend sends the page data
If the user can't: your backend sends "not authorized" message and your frontend redirects
This way even if someone manipulates your client-side code and cancels the redirect, the hacker will stare at a useless blank page.
In summary:
Your approach on the client-side is fine. Make sure your backend checks the identity of your user before sending the company data.

So instead of me manually handling the routing, I solely used ReachRouter's navigate to simulate history.push() to avoid re-rendering of the whole page. (similarly just a state change with the benefit of keeping track of history ). Backend is fully protected with auth tokens so no need to worry about that.
My strategy below will handle these cases:
User types in or app navigates to /company ( app will pre fetch > get first company by default > navigate to /company/{id}
User types in or app navigates to /company/{id} ( app will pre-fetch > navigate to /company/{id} > component triggered by that route will check validity of id else navigate to 404 )
The strategy I created was,
Create a component that will load up a loading screen by default and prefetch the said companies.
Call the said component in the pages folder as the default, in my case, /company should be pages > company.js or pages > company > index.js
If the prefetch is successful, navigate to /company/{id} which is the child of the said component, which is a purely client route.
make sure gatsby-node.js have the necessary createPages definition to allow client routing for everything /company/*
manually check if the current location is /company to deny redirection to capture the second case when user types in /company/{id}
Better if I show the code, ignore my custom built in hooks.
useRequest just gives an axios class, with the true parameter telling it's an authenticated required request.
useApi just gives a handy (async-await) function that includes dispatch for the redux states and calling the api itself. I'm using thunk, so success and error are standard.
const CompanyDefault = ({location}) => {
const [loading, toggle] = useState(true)
const request = useRequest(true)
const companyApi = useApi(request, getCompanies)
const companyState = useSelector(({company}) => company)
const redirectToCompany = async () => await navigate(clientRoutes.COMPANY(companyState.data[0].id))
const fetchCompanies = async () => {
await companyApi()
toggle(false)
}
useEffect(() => {
if(companyState.data.length === 0){
fetchCompanies()
}
}, [])
if(loading){
return <Loading />
}
else if(companyState.error || companyState.data.length === 0){
return <Error callApi={companyApi} />
}
else if(companyState.success && (location.pathname === "/company" || location.pathname === "/company/")){
redirectToCompany()
}
return (
<Router>
<Company path="company/:companyId/*" />
</Router>
)
}
export default CompanyDefault
Thus, the company module will be just
const Company = ({companyId}) => (
// you now have companyId!
// do magic here, and check if that company id is valid.
<Router>
<Main path="/">
<Summary path="/" />
.... other dashboard routes
</Main>
</Router>
)
Hopefully this is cleaner. If there's a better way do let me know! :D

Related

React JS - How do I prevent users from tampering their cookies and gaining access to protected routes/endpoints?

I have a React app wherein the user object is stored as cookies in the App component. This user object has a property AccountType which is an integer, and 1 means that the user is a student and 2 means the user is a teacher.
I am using react-router v5 to protect certain routes from being accessed, unless the logged in user is of AccountType 2 (teacher) with the following RouteGuard component:
const RouteGuard = ({ component: Component, ...rest }) => {
const user = JSON.parse(Cookies.get("loggedInUser"));
return (
<Route
{...rest}
render={(props) => {
return user.AccountType === 2 ? (
<Component />
) : (
<Redirect
to={{
pathname: "/403",
state: {
error: "You are not allowed to access this resource.",
from: props.location.pathname,
redirected: true,
},
}}
/>
);
}}
/>
);
};
And an example of how this RouteGuard component is used:
<Switch>
<RouteGuard path="/records" exact component={Records} />
</Switch>
It works well, for normal cases, but I found out that I can login as a student and go to the developer console and in the cookies section, I can modify the cookies and manually set AccountType to 2, thereby bypassing the route protection.
What would be the proper way of preventing unauthorized users from tampering cookies and gaining access to protected endpoints, front-end wise?
There is no way to disallow this. The best method would be to store a version of any username in the local storage, then compare that data to a server-side database to figure out if that user has the required account type.
This is a similar question:
How to secure localStorage in HTML5?
If you get this value from the backend and store it in local storage it will solve your problem. But The best approach will be authenticate using the JWT token and pass the necessary info into it.

Secure authentication structure in a React Webapp

I am currently learning React and worked through some courses but still haven't completely understood how to create a proper structure for a secure web app.
For the sign in, sign up flow I use the firebase SDK. Once logged in, a user gets redirected to a private route. Right now I only have 2 user roles. Guests and signed in Users. This enables me to create private routes by using an inbuild firebase function. This is the first problem as it is not scalable once I add different roles as it would force me to send a request to the backend to check what role the user is and thus which pages he can acces.
if (firebase.auth().currentUser === null) {
console.log("not logged in")
return (<Redirect
to={{
pathname: "/signin",
state: {
from: props.location
}
}}
/>);
}
So I thought that the easiest option would be to use Context, which did work. Once a user loggs in, the server sends a user object which the app refers to for the rest of the session. I followed a bunch of tutorials and they all had the same problem that when using chrome developer tools with the react features, you could just edit the state of the user and bypass the private routes etc.
Second Try:
<UserContext.Consumer>{(context)=>{
const {isLoggedIn} = context
return(
<Route
{...rest}
render={props => {
if (isLoggedIn) {
console.log("not logged in")
return (<Redirect
to={{
pathname: "/signin",
state: {
from: props.location
}
}}
/>);
I'd be grateful if somebody could point me in a direction as it seems like I am missing something important.
EDIT 1: Or is it simply that once you build the app, you can no longer access these states and it's considered safe?
when using chrome developer tools with the react features, you could just edit the state of the user and bypass the private routes
Your routes will never be truly private. They are part of the JavaScript bundle that gets downloaded and rendered by the browser, so they should never contain anything secret. Anyone could read this code if they really wanted to.
Consider this:
if (loggedIn) {
return <div>Secret data: ABC</div>;
}
The string "ABC" is contained in your app build, and is not really a secret anymore. The average user wouldn't know how to obtain it, but a developer probably would, for example by toggling some state in the developer console.
However, the data that comes from Firestore (or any another backend service) should be properly protected. Permission checks are done server-side before this data is sent to the browser. So, unless the user has the required permissions, the data will never be exposed to the wrong person, even if someone tampers with your client-side code in the developer console.
if (loggedIn) {
fetchDataFromBackend();
}
It doesn't matter if someone changes loggedIn to true so that fetchDataFromBackend() is called; the server will make sure the data isn't returned unless the user has the proper permission (e.g. is logged in). In the case of Firebase (Firestore), this protection is achieved with Security Rules.
And, by the way, the recommended way to get the current user with Firebase is to add a listener to the Auth object:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
You could put this in a top-level component and share the user object with child components through a context. That way you don't have to call firebase.auth() all over the place. Here's a good starting point if you need some inspiration: https://usehooks.com/useAuth/
I think what you are doing on the frontend site is good, but you would also need logic in the backend to protect your routes. This means that an user may be able to circumvent your route protection via dev tools on the frontend, but your backend would only send error messages to him, as it recognizes that he has no allowance.
You could do this with Higher Order Functions like this one:
const authenticationWrapper = createResolver(
async ( models, session, SALT ) => {
try {
if (!session) {
throw new Error("No valid credentials!");
}
const { id } = verify(session.token, salt);
const valid = databasecall //
if (!valid) {
throw new Error("No valid user!");
}
return true
} catch (error) {
session.destroy((err) => {
if (err) {
console.error(err);
}
});
}
}
);
All private backend functions would be wrapped into and authentication of the user would be checked every time.
The principle to check in Front- and Backend is called dual authentication. You can read more about it here.

React - Redirect to third party website when submit form

I am creating a login form and when it is submitted, I am redirecting the user to an API website to get a token to access the data.
I am trying to render the following:
if(this.state.isSubmitted) {
return (
<Router>
<Redirect to={`${authEndpoint}?
client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scopes.join(
"%20")}&response_type=token&show_dialog=true`} />
</Router>
);
}
The error I am getting is:
Uncaught Invariant Violation: <Router>: Children of <Router> must have a `path` or `default` prop, or be a `<Redirect>`. None found on element type `function Redirect(_ref)
Can anyone tell me how to fix this please?
You need to nest your Redirect inside a Route component as described here
But what you are trying to achieve here is wrong. Redirect is a component used for routing react pages inside your project. If you want to redirect the user to a different website use
if(this.state.isSubmitted) {
window.location.href = `${authEndpoint}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scopes.join("%20")}&response_type=token&show_dialog=true`
}
If you want to redirect the user when they visit a specific route of your website use this answer instead

How to redirect the application to home page when refreshing the browser?

I have a ReactJS application with two pages:
1 - Home Page, where the code requests characters data from Star Wars API (https://swapi.co/);
2 - Characters Page, where the characters data is showned, based on the data extracted from the API in the home page.
If I refresh the page when I'm in home page, that's ok. The data will be requested again. But if I'm in the characters page, it crashes because the characters page doesn't request any data. It works with data already collected in home page.
How can I redirect the user to home page if he or she reloads it in the characters page?
I'm using react-router-dom and the home page url is set as "/" and the characters page is "/characters".
I'm also using Redux to store the data.
Thanks.
EDIT: I added the source code for a React/Redux application example that uses axios and redux-thunk to get data from the API.
Are you using connect() from react-redux on your Characters Page component?
If not, you can do the following:
import { connect } from 'react-redux'
const CharactersPage = props => {
// You can retrieve your data from props.characterData
return ...
}
const mapStateToProps = state => {
return { characterData: state.characterData }
}
export default connect(mapStateToProps)(CharactersPage)
This should solve the issue of your Characters Page crashing on reload because it doesn't have the data.
And if you really need to redirect users to the / page, you can utilize the history object. It automatically gets passed down when you set up your routes using <Route> from react-router-dom, given that you set up your route as such:
<Route path='/characters' component={CharactersPage} />
If you use render={} instead, you can get the history object by doing:
<Route path='/characters' render={routeProps => <CharactersPage {...routeProps} />} />
I hope this helps!

How to save Information of user in React after user login

I write a small app with React and back-end is PHP. I have 2 type user in database (admin and student). After user login, I save information in session Storage like that ( user: { username:'abcxyz123', role: 'admin' } ). The component render based user.role. This working good. But If I open DevTools and change user.role, my app will wrong render (because user.role is very simple text). How can I avoid this ??? My code is look like that.
class MyApp extends Component {
constructor(props) {
super(props);
this.state = {
user: (window.sessionStorage.user)
? JSON.parse(window.sessionStorage.user)
: false,
};
}
checkUserToLogin = (e) => {
if( loginSuccess ){
// return data of user in variable finalData
// finalData = { username:'abcxyz123', role: 'admin' }
window.sessionStorage.setItem('user', JSON.stringify(finalData));
this.setState({ user: JSON.parse(window.sessionStorage.user) });
}
}
render() {
const {
user
} = this.state;
return (
<div>
<form onSubmit={ this.checkUserToLogin }>
<input type="text" />
<input type="password" />
<button type="submit"> Login </button>
</form>
{/*Component will render based props user.role */}
<Component user={user} />
</div>
)
}
}
I can't change my database. The data of role always 'admin' or 'student'.
If a check is done from your backend on every authenticated action, it shouldn't be a problem.
What I think you do wrong
I think you send authenticated information to the front and let it handle if they should be shown or not. Which is really bad. Every informations sent in request can possibly be read, even if it's not render in DOM. The php backend should filter information based on database role.
The solution
Only keep a token or something that authenticates your front user in its XHR request. JWT is a really great way to do it cause it can not be mutate from the front.
Handle the render or not of admin action but continue to check it in every backend request.
If the information are changed by a malicious user, it's going to be kick from backend and you don't care if the front is broken for him.
To go further
It can be interesting too to keep this token and information in a global context. For example you can use React.Context or Redux and synchronise it with your local storage.
So you don't need to go through props driling with your user data.

Categories