User authentication in React Router - reactjs

folks. I'm learning how to integrate React with Express using React Router, and I've run into a problem with authenticating users. I'm trying to use a higher order component to conditionally render a protected route based on a user's authorization status.
const ProtectedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
if (!AUTHORIZED) {
return <Redirect to="/login" />;
}
return <Component {...props} />;
}}
/>
);
};
The problem I'm having is in the if (!AUTHORIZED) statement. I'm using Passport to handle authentication on the Express server side, and I have an endpoint set up for retrieving user information and authorization status, but I can't figure out how to get access to that data before the page renders. If I was using a class component instead of a functional component, (learning hooks also), I think I could get the data with the componentWillMount lifecycle method, but I read that's bad practice. Any ideas on how I could move forward from here would be much appreciated!
***edit***
A couple of things I tried to get this working...
I tried adding an authorization module to fetch the data for me.
class Auth {
constructor() {
this.authenticated = false;
}
async isAuthenticated() {
console.log("hitting auth route");
await fetch("/api/auth")
.then(res => res.json())
.then(json => {
if (json.error) {
this.authenticated = false;
}
this.authenticated = true;
});
return this.authenticated;
}
}
export default new Auth();
I import the module and plug auth.authenticated() in place of the placeholder AUTHORIZED. This function gets skipped, because it's asynchronous, and the redirect will always occur.
So I need to add await to auth.authenticated(). But now I need to have async further up the chain, so I foolishly add async in front of props, as such:
render={async props => {
So now It's trying to render a promise object instead of a component, and we get the error Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
Promises all the way down.

Answering this in case anyone runs into a similar issue...
The first solution I had was based on Firealem Erko's comment. On login, I saved a variable with the user's ID to local storage and referenced that in my component. This was a good first solution, but later was improved by something rotimi-best mentioned in his comment. It turns out you can indeed pass props to these components, which I did not realize in my inexperience. So that is now the way I'm doing it. The final solution is as follows:
const ProtectedRoute = ({
component: Component,
logged,
setLogged,
...rest
}) => {
return (
<Route
{...rest}
render={props => {
if (!logged) {
return (
<Redirect
to={{
pathname: "/login",
state: { flashInfo: "Please log in to continue." }
}}
/>
);
} else {
return <Component {...props} logged={logged} setLogged={setLogged} />;
}
}}
/>
);
};
And here's the parent component where I'm passing in the props:
function App() {
let [logged, setLogged] = useState(false);
useEffect(() => {
if (window.localStorage.getItem("qrs")) {
setLogged(true);
} else {
setLogged(false);
}
}, []);
return (
<div className="App">
<BrowserRouter>
<Nav logged={logged} setLogged={setLogged} />
<Switch>
<ProtectedRoute
exact
path="/dashboard"
component={Dashboard}
logged={logged}
setLogged={setLogged}
/>
<Route
path="/register"
exact
render={props => <Register {...props} logged={logged} />}
/>
<Route
path="/login"
exact
render={props => (
<Login {...props} logged={logged} setLogged={setLogged} />
)}
/>
</Switch>
</BrowserRouter>
</div>
);
}
Thanks to all the commenters for their suggestions!

Related

How to use Protected Routes with react-router-dom V6 and typescript?

I am trying to implement a protected route to only allow logged in users to access my app. The code I have written seems to work, I am redirected to my login page when I try to access the homepage without being logged in, however once I am logged in I can access the page but I does not render and I get the following error: Click here for error
I have tried multiple methods and wrapping the element in my protected route seems like the V6 way of doing things, however it doesn't seem to work for me:
My protected route
interface PrivateRouteProps extends RouteProps {
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({...rest}) => {
const auth = useAuth();
if (auth?.user === null) {
return <Navigate to='/'/>;
}
return <Route {...rest}/>;
};
export default PrivateRoute;
My app (route usage)
function App() {
useEffect(() => {
API
.get('api', '/reservation', {})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error.response);
});
}, [])
return (
<Router>
<Routes>
<Route path='/' element={<LoginPage />}/>
<Route path='/consultAndReserve' element={<PrivateRoute><Navbar/><ConsultReserve/></PrivateRoute>} />
<Route path='/consultAndReserve/:date' element={<><Navbar/><ConsultReserveWithDate/></>}/>
<Route path='/myReservations' element={<><Navbar/><Reservations/></>}/>
<Route path='/tracing' element={<><Navbar/><Tracing/></>}/>
</Routes>
</Router>
);
}
What am I doing wrong?
It's fairly trivial to create a "PrivateRoute" component, even in TypeScript.
In your case you can't directly render a Route component as the error points out. This was a breaking change between RRDv5 and RRDv6. You can render the children prop since you are directly wrapping the components you want to protect.
Example:
const PrivateWrapper = ({ children }: { children: JSX.Element }) => {
const auth = useAuth();
return auth?.user ? children : <Navigate to="/" replace />;
};
Usage:
<Routes>
...
<Route
path="/consoleAndReserve"
element={(
<PrivateWrapper>
<Navbar />
<ConsultReserve />
</PrivateWrapper>
)}
/>
...
</Routes>

Is it bad approach to make wrappers for public/private routes in react-router v6 like this

In app I know that user is authenticated when there is token in redux.
When app loads I first fetch that token from server.
I made a wrapper for private and public routes.
I made wrapper for public routes because if user is on login page and reload app once token is fetched I need to make redirect to authenticated page.
What troubles me is this approach ok and does it make performance problems for the future?
Without public route wrapper user is able to access login page when authenticated (which should not).
I am trying to make a clean solution as possible.
This is my routes rendering:
return (
<Routes>
{routes.map((route, index) => {
if (route.isProtected) {
return (
<Route
key={index}
path={route.path}
element={
<RequireAuth> <<<< wrapper
<route.component />
</RequireAuth>
}
/>
);
} else {
return (
<Route
key={index}
path={route.path}
element={<Public><route.component /></Public>} <<<< wrapper
/>
);
}
})}
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
Public wrapper
function Public({ children }: { children: JSX.Element }) {
const { state } = useLocation();
const {token, tokenChecking} = useAppSelector((state) => state.auth);
if (tokenChecking) {
return (
<div>app is loading...</div>
)
}
if (token) {
return <Navigate to={state?.from || "/dashboard"} replace/>;
}
return children;
}
Private route is more or less reverse
function RequireAuth({ children }: { children: JSX.Element }) {
let location = useLocation();
const {token, tokenChecking} = useAppSelector((state) => state.auth);
if (token) {
return (
<div>App is loading ...</div>
)
}
if (!token) {
return <Navigate to={"/login"} state={{ from: location }} />;
}
return children;
}
What troubles me is this approach ok and does it make performance
problems for the future?
This approach seems reasonable to me and I don't foresee any performance issues with it.
If you want to make the code a bit more DRY then I suggest looking at the isProtected flag first and instantiate the wrapper first, then render a single wrapper and component. Remember that React components are PascalCased, so create a Component to render into the wrapper.
return (
<Routes>
{routes.map((route) => {
const Wrapper = route.isProtected ? RequireAuth : Public;
const Component = route.component;
return (
<Route
key={route.path}
path={route.path}
element={
<Wrapper>
<Component />
</Wrapper>
}
/>
);
})}
<Route path="*" element={<NotFoundPage />} />
</Routes>
);

Protected routes in react-redux app, React router is not working as expected

I have passport strategy with express server on backend. I have set up all the passport strategies and authentication. Now I need to protect some of my Routes. I am using redux to store the status whether the user is logged in or not.
I have tried creating a Private route according to this tutorial. But it got into infinite loop (infinite rendering of the component).
Following code successfully connects to redux store , dispatches an action, and backend is also working fine.
But please suggest me why am i into infinate loop.
My Protected function looks like :
const login = {
type: "Login"
};
const logout = {
type: "Logout"
};
const AUTH =
process.env.NODE_ENV === "production"
? "https://birdiez.herokuapp.com/auth/check"
: "http://localhost:3090/auth/check";
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props => {
console.log("calling api");
fetch(AUTH)
.then(response => {
return response.json();
})
.then(data => {
console.log("Promise resolved:", data);
data.msg === "OK" ? store.dispatch(login) : store.dispatch(login);
});
const { status } = store.getState();
return status === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: { from: props.location }
}}
/>
);
}}
/>
);
export default PrivateRoute;
My App.js looks like :
class App extends Component {
render() {
return (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<PrivateRoute path="/dash" component={Dash} />
<Route exact path="*" component={Home} />
</Switch>
</Router>
);
}
}

Protected Route in React, not passing parameters

I am trying to create a protected route as shown in the code below:
const PrivateRoute = ({component:Component, authenticated, ...rest}) =>{
console.log(authenticated);
return(
<Route {...rest} render={(props) => (
authenticated === true
? <Component {...props} {...rest} />
: <Redirect to="/Login"/>
)} />);
}
export default PrivateRoute;
I am passing the following params in my Router configuration:
<PrivateRoute component={Appointments} authenticated={this.state.authenticated} exact path="/appointments" render={(props) => <Appointments {...props} appointments={this.state.appointments} />} />
.
However, when I try routing, it appears that the "appointments={this.state.appointments}" prop is not getting passed to the "Appointments" component.
This is the error that I get
TypeError: Cannot read property 'map' of undefined
Any idea what the issue could be?
I think the problem here is that you are passing props in the render property of the PrivateRoute which is as render={(props) => <Appointments {...props} appointments={this.state.appointments} />}. Now this property is not being Utilised in your actual component rendering in the PrivateRoute. Try following during your initialisation of route:
<PrivateRoute component={Appointments} authenticated={this.state.authenticated} exact path="/appointments" appointments={this.state.appointments} />
This should fix your issue. I would suggest here that rather than creating a PrivateRoute of your own, you can use a React HOC to create an authentication wrapper on the actual component.
function PrivateRoute({ children, ...rest }) {
const isAuthenticated = (location) => {
//if loading show loading indicator
}
const childrenWithLocation = (location) => Children.map(children, child => {
if (isValidElement(child)) {
return cloneElement(child, { to: child.props?.to + location.search });
}
return child;
});
return (
<Route
{...rest}
render={({ location }) =>
isAuthenticated(location)
? (childrenWithLocation(location))
: (<Redirect
to={{
pathname: "/login" + location.search,
state: { from: location }
}}
/>)
}
/>
);
}

Redirect to server route on ReactJS component using React Router V4

I have my React Router V4 routes structured this way:
const isAuthenticated = () => {
let hasToken = localStorage.getItem("jwtToken");
if (hasToken) return true;
return false;
};
const AuthenticatedRoute = ({ component: Component, ...rest }) =>
<Route
{...rest}
render={props =>
isAuthenticated()
? <Component {...props} />
: <I NEED TO REDIRECT FROM HERE TO SERVER PAGE />}
/>;
class App extends Component {
render() {
return (
<BrowserRouter basename="/editor">
<Switch>
<AuthenticatedRoute exact path="/" component={AppNav} />
<AuthenticatedRoute
exact
path="/:module"
component={AppNav}
/>
<AuthenticatedRoute
exact
path="/:module/:screen"
component={AppNav}
/>
<AuthenticatedRoute
exact
path="/:module/:screen/:action"
component={AppNav}
/>
<AuthenticatedRoute
exact
path="/:module/:screen/:action/:id"
component={AppNav}
/>
<Route component={PageNotFoundError} />
</Switch>
</BrowserRouter>
);
}
}
export default App;
As you see on code, if not authenticated I want to redirect to server page. The page is another react application to manage user registration and is located in the server but in another route tree: /registration
What I've tried with no success:
<Redirect to="//registration' />
windows.location = "/registration"
windows.location.href = "/registration"
<Redirect to="http//registration' />
windows.location = "http://registration"
windows.location.href = "http://registration"
All of the redirects to page in current application.
What would be the solution for this ?
I had a create-react-app project with react router, and the problem that when entering a path '/path/*' it loaded the as if in the same react router, even though I had configuration for using another react project with an express server.
The problem was that the service worker was running in the background, and for any route inside '/' it was using cache.
The solution for me was to modify the 'registerServiceWorker.js', so that when you are inside the path, you ignore the service worker. I didn't get too deep in learning about service workers, but here is the code I used:
export default function register() {
...
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if(window.location.href.match(/*\/path\/*/)){
// Reload the page and unregister if in path
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else if (isLocalhost) {
...
When inside the path, it unsubscribes the service worker and reloads the page.
I implemented it like so:
const AuthenticatedRoute = ({ component: Component, ...rest }) =>
(<Route
{...rest}
render={props =>(
isAuthenticated()
? (<Component {...props} />)
: ( window.location = "http://your_full_url" )
)}
/>);

Resources