React Redux Auth Middlware - reactjs

I am doing a login application using react/redux, here is my flow.
User logs in using email/password pair and receives a token, which I
store in the cookie.
Once I have the token, I can get his credentials by doing a request on an /auth endpoint which retrieves his personal details - firstname, lastname, email.
I have a PrivateRoute which used to do the auth check, however I want to trigger the auth check on all routes, not only the private ones.
This way if the user is viewing the homepage I can still show his firstname in the navigation (for example)
The main issue seems to be the proper place to call the auth action.
App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { Provider } from 'react-redux';
import store from './store';
// Styling
import './App.css';
// Privat route
import PrivateRoute from './routes/PrivateRoute';
// Common
import Navigation from './components/common/Navigation';
// Components
import Login from './components/Login';
import Home from './components/Home';
import Profile from './components/Profile';
import ArticlesList from './components/Profile/ArticlesList';
import ArticleForm from './components/Profile/ArticleForm';
const App = () => (
<Provider store={store}>
<Router>
<Switch>
{/* Public routes */}
<Route exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
{/* Private routes */}
<PrivateRoute exact path="/profile" component={Profile} />
<PrivateRoute exact path="/profile/articles" component={ArticlesList} />
<PrivateRoute exact path="/profile/articles/new" component={ArticleForm} />
<PrivateRoute exact path="/profile/articles/:id(\d+)" component={ArticleForm} />
</Switch>
</Router>
</Provider>
);
export default App;
Here is a piece of my userActions.js where the auth action is defined
export const auth = () => async dispatch => {
dispatch({ type: AUTH_USER_REQUEST });
try{
let data = await UserService.auth();
dispatch({
type: AUTH_USER_SUCCESS,
payload: data
});
}catch(err){
dispatch({
type: AUTH_USER_ERROR,
payload: err.message
});
}
}
One of my ideas was to create a parent Route class to do the routing, and call the auth there.

To overcome it, I did something like the following by using Redux:
<Switch>
<Route
path="/"
component={this.props.auth.authenticationInProgress ? BlankPage :
(this.props.auth.isAuthenticated ? SegmentPage : LoginPage)}
exact={true}
/>
<Route component={NotFoundPage}/>
</Switch>
If the user logged in, the Route renders SegmentPage. Otherwise it renders LoginPage. Once login or logout process is triggered, the authentication is toggled and the page renders accordingly. Also I keep the status of authentication process so that no private data is shown to the user during the authentication check.

Related

Setting routes for BrowserRouter in tests

I have a test case where I need to start on the Signup page (/signup), and on successful signup, the application should navigate to the Home page (/). I am currently using the MemoryRouter to set the initial route to /signup and everything works well. See below:
import React from 'react';
import userEvent from '#testing-library/user-event';
import { MemoryRouter } from 'react-router';
import { Route, Routes } from 'react-router-dom';
import { render, screen } from '../../test/test-utils';
import { SignUpPage } from './SignUpPage';
const MockHomePage = () => <div>MockHomePage</div>;
describe('<SignUp />', () => {
test('navigates to Home page on successful signup', async () => {
render(
<MemoryRouter initialEntries={['/signup']}>
<Routes>
<Route path="/" element={<MockHomePage />} />
<Route path="/signup" element={<SignUpPage />} />
</Routes>
</MemoryRouter>
);
// Enter valid user info and submit form
userEvent.type(screen.getByLabelText('Full Name'), 'John Smith');
userEvent.type(screen.getByLabelText('Email'), 'johnsmith#gmail.com');
userEvent.type(screen.getByLabelText('Password'), 'let-me-in');
userEvent.type(screen.getByLabelText('Confirm Password'), 'let-me-in');
userEvent.click(screen.getByText('Sign up'));
// Expect to see the Home page
expect(await screen.findByText('MockHomePage')).toBeInTheDocument();
});
});
However, I'd like to use BrowserRouter in my tests instead of MemoryRouter (I understand that it is the recommended way since it is closer to what the real app would use). Unfortunately, BrowserRouter does not have the initialEntries prop (I am using React Router v6 beta.7). Is there a way to force BrowserRouter to start at /signup?
I tried the location API like this:
window.location.href = 'http://localhost:3000/signup';
However I get a jsdom error saying that this API is not implemented:
Error: Not implemented: navigation (except hash changes)
at module.exports (/Users/naresh/projects/accelerated-news/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17)
Is there any other way?
Using react-router-dom v6.
I believe you need to use window.history.pushState() so it would look something like this:
test('navigates to Home page on successful signup', async () => {
window.history.pushState({}, '', '/signup')
render(
<BrowserRouter>
<Routes>
<Route path="/" element={<MockHomePage />} />
<Route path="/signup" element={<SignUpPage />} />
</Routes>
</BrowserRouter>
);
// etc..
});

How to push my router link as login link when the token expired using react router dom?

How to do pushing my router link to /login when the token expired?. Right now when my token expired but the link url still on the last history that I stopped. I don't know what this problem called but I put my goal and current output.
Goal: If the token expired the router link will be push on /login not on the last Link that I stop.
Library Use:
Redux Toolkit
React Router Dom
Imports:
//router
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useHistory
} from "react-router-dom";
//components
// import Header from './components/Header';
import MainLayout from './components/MainLayout';
//not found
import NotFound from './pages/NotFound';
//unaunthenticated user
import Login from './pages/Login';
//authenticated user
import Dashboard from './pages/Dashboard';
import CustomerPayor from './pages/CustomerPayor';
App.js File:
function App() {
let user = useSelector(selectUser);
let history = useHistory();
return (
<div className="App" >
{!user ? (<Login />) : (
<Router>
<MainLayout userProfile={user}>
<Switch>
<Route path="/" exact component={Dashboard}>
</Route>
<Route path="/customer_payor" exact component={CustomerPayor}>
</Route>
<Route path="*" component={NotFound} />
</Switch>
</MainLayout>
</Router>
)}
</div>
);
}
export default App;
As you can see here the token already expired and the page return to the component of login but my link is on customer_payor
For this case, you can use Redirect:
<Route exact path="/">
{loggedIn ? <Redirect to="/dashboard" /> : <Redirect to="/login" />}
</Route>
Perform a check in the useEffect of the parent state provider to decrypt eg. jwt token and if it's expired you log the user out.
Then you use the useLocation hook from react router to do location.push('/login) afterwards

React Router gives a blank screen on protected Route

So essentially, I am trying have an authentication workflow. So basically, the Home route is protected, login is not, register is not, and then I have a verifyEmail page that opens if you arent verified.
const PrivateRoute = ({component: RouteComponent,...rest}) =>{
const{currentUser}=useContext(AuthContext)
function route (){
}
return(
<Route
{...rest}
render={routeProps =>
!!currentUser && currentUser != null
?
currentUser.emailVerified ? <RouteComponent {...routeProps}/>:(<Redirect to={"/verifyEmail"}/>)
:
(<Redirect to={"/login"}/>)
}
/>
)
}
export default PrivateRoute
And then in App.js I have
function App() {
return (
<div className={'App'}>
<AuthProvider>
<Router>
<div>
<PrivateRoute path="/" component={HomePage}/>
<Route path="/verifyEmail" component={Verify}/>
<Route path="/login" component={Login}/>
<Route path="/register" component={Register}/>
</div>
</Router>
</AuthProvider>
</div>
);
}
export default App;
Current User is basically the user credentials, I am using Firebase for authentication. The problem I am having is that after logging in it just shows a blank screen, when the email is not verified instead of showing the verifyEmail page.
To elaborate more on the problem, the actual problem is that if a user is not email verified, then it routes to nowhere, and gives me a blank screen so meaning <Redirect to="/verifyEmail" doesnt work. To debug this further I decided to replace that with a Hello</> and I saw a screen with Hello. So I dont think authentication is the problem, just that it doesnt Route to the appropriate page.
Please help.
You can create a HOC (a component) that wrap your Routes and you make your validation there.
Example:
import React, { useEffect } from 'react'
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
export default function RequireAuth({ children }) {
const history = useHistory();
const auth = useSelector(state => state.auth)
useEffect(() => {
if(!auth){
history.push("/")
}
}, [auth])
return (
<>
{children}
</>
)
}
This is an example of a personal project where I use react-redux to take auth witch is only a boolean.
I use useHistory of react-router-dom to redirect to "/" in case the user isn't logged in.
And finally in my App.js:
<div>
<RequireAuth>
<Route path='/post' component={PostPage} />
</RequireAuth>
<Route path='/' exact component={LoginPage} />
</div>

Protecting routes in React app with React Router

I've created a simple React app with Redux, React Router and Auth0 which handles user authentications.
I'm trying to create this basic behavior to control access:
All unauthenticated users will automatically be sent to /public
Authenticated users can access all the other parts of the app
Once a user is authenticated by Auth0, I want to process the access_token and send user to / which is the Home component
Everything is "almost" working the way it should. The problem I'm having is that render() function in App.jsx is executing BEFORE the lock.on('authenticated') listener even has a chance to process the tokens returned by Auth0. As a result, the tokens are never stored and the user always seems to be unauthenticated. If I send user to /login, everything works fine because I'm not checking to see if the user is authenticated before rendering the Login component.
I think the way I'm handling protected routes needs to change. Any suggestions as to how to handle protected routes?
I'm providing the code that you need here. If you want to see the whole app, go to https://github.com/imsam67/react-redux-react-router-auth0-lock
The following is the App.jsx:
class App extends Component {
render() {
const isAuthed = isAuthenticated();
return (
<div>
<Switch>
<Route exact path="/" render={ props => isAuthed ? <Home {...props} /> : <Redirect to="/public" /> } />
<Route exact path="/login">
<Login />
</Route>
<Route path="/public">
<Public />
</Route>
</Switch>
</div>
);
}
}
This is the AuthWrapper component where I handle Auth0:
class AuthWrapper extends Component {
constructor(props) {
super(props);
this.onAuthenticated = this.onAuthenticated.bind(this);
this.lock = new Auth0Lock('my_auth0_client_id', 'my_domain.auth0.com', {
auth: {
audience: 'https://my_backend_api_url',
redirectUrl: 'http://localhost:3000/',
responseType: 'token id_token',
sso: false
}
});
this.onAuthenticated();
}
onAuthenticated() {
debugger; // After successful login, I hit this debugger
this.lock.on('authenticated', (authResult) => {
debugger; // But I never hit this debugger
let expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
sessionStorage.setItem('access_token', authResult.accessToken);
sessionStorage.setItem('id_token', authResult.idToken);
sessionStorage.setItem('expires_at', expiresAt);
});
}
render() {
return(
<AuthContext.Provider value={{ lock: this.lock }}>
{this.props.children}
</AuthContext.Provider>
);
}
}
And here's index.js in case you need to see it:
import App from './components/App';
import AuthWrapper from './components/auth/AuthWrapper';
// Store
import appStore from './store/app-store';
const store = appStore();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<AuthWrapper>
<App />
</AuthWrapper>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
Here are the changes I made to make this work.
I now initialize Auth0 Lock in index.js to make it global
I moved the onAuthenticated() listener to LoginCallback.jsx component which is where the user is sent after a successful login. I believe moving the onAuthenticated() from App.jsx to LoginCallback.jsx made the biggest impact because the listener in LoginCallback.jsx executes before App.jsx.
On successful authentication, I also use this.props.history.push('/'); to send user to Home component
For full code, please see the repo at https://github.com/imsam67/react-redux-react-router-auth0-lock
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed(assuming they can't get any data without a token) and redirect if they landed there even after your router checks for auth or shows an error.
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed and redirect if they landed there even after your router checks for auth. I think #Sam has it right. The routes may not respond as expected to an asynchronous call changing it or may have odd behavior. I've never attempted a dynamic route this way but always had conditional renders of content components. A better approach may be to send the call and in the then block redirect to a url which the router knows to handle. Just not sure the router handles this very well. Catch the component checking for auth on load and redirect back to log on if not authorized. Sorry I'm not helping much here but conditional routes almost seem like an anti pattern but I guess it could work if we knew how the router renders its data after changes or if it actually does at all(the routes them selves.) So if they were to bookmark the url and try to return back that would be a 404 right? Maybe like a 401 unauthorized showing and redirect or link to log in might be better?
Dynamic routing need to be defined outside of the <Switch> scope. Here is an exemple assuming your function isAuthenticated() is a state (Redux or wathever)
import React from "react";
import ReactDOM from "react-dom";
import { createBrowserHistory } from "history";
import { Router, Route, Switch, Redirect } from "react-router-dom";
// core components
import Admin from "layouts/Admin.js";
import SignIn from "layouts/SignIn";
const hist = createBrowserHistory();
const loggedRoutes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/admin" component={Admin} />
<Redirect from="/admin" to="/admin/aboutUs/whatWeDo" />
</Switch>
);
const routes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/login" component={Admin} />
<Redirect from="/" to="/login" />
</Switch>
);
ReactDOM.render(
<Router history={hist}>
{checkIfAuth? loggedRoutes():routes()}
</Router>,
document.getElementById("root")
);
In this exemple, If you are not login you are redirect to /login.

React Router Dom Private Route Always Redirects to Login

I’m building a full stack react application. I have the stack operating and pulling information from the backend when I’m calling it from the frontend. I’m using axios on the frontend to hit the endpoints I’ve set up on the backend. I’m having an issue with frontend authentication though. I’m using passport and passport-google-oauth20 to log users in with Google OAuth2.0. I have this working correctly. The problem I’m having: when a user hits a specific URL (controlled by react-router-dom) I have a function called authVerify run that pings the backend and checks if the user is logged in (by looking at the cookie - not accessible in JS). The function runs correctly and correctly updates the state. I have a state field for authenticated originally set to false, and on a successful 200 response I setState for authenticated to true. But, the protected routes I’ve built aren’t working. If the user is authenticated, they should be able to go to that route. If not, they get redirected to the login page. But, everything just redirects to the login page.
Here is the Router.js code:
import React from 'react';
import axios from 'axios';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';
import Login from './Login';
import SignUp from './SignUp';
import App from './App';
import NotFound from './NotFound';
class Router extends React.Component {
constructor(props) {
super(props);
this.state = {
authenticated: false,
};
}
componentDidMount() {
this.authVerify()
.then((res) => {
if (res.status === 200) {
console.log(this.state);
this.setState({
authenticated: true,
});
console.log(this.state);
}
})
.catch(err => console.log(err));
}
authVerify = () => axios.get('/authVerify')
render() {
return (
<BrowserRouter>
<Switch>
<PrivateRoute path="/" component={App} />
<Route exact path="/pages/signup" component={SignUp} />
<Route exact path="/pages/login" component={Login} />
<PrivateRoute path="/pages/dashboard" authenticated={this.state.authenticated} component={App} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
}
}
export default Router;
And here is the PrivateRoute.js code:
import React from 'react';
import { Route, Redirect, withRouter } from 'react-router-dom';
const PrivateRoute = ({ component: Component, authenticated, ...rest }) => (
<Route
{...rest}
render={props => (authenticated === true ? <Component {...props} /> : <Redirect to="/pages/login" />)}
/>
);
export default withRouter(PrivateRoute);
I have console logged the response from the API and I successfully get a 200 response. I’ve also console logged the state before and after the call to the API and before it’s false and after it’s true. But, I’m always redirected to the login page.
I think the issue is that the call to the API is taking too long and the component is mounting before authentication is verified. But, I thought by setting the state in the componentDidMount function would update the component and redirect appropriately. Any help would be appreciated.
I had the same problem when working with private routes, the issue is that your component is rendered before the resolution of your API call, the solution for me was to wait for the response to finish before rendering the component.
I used a isSendingRequest which starts as true and prevented my component from rendering (maybe you can display a spinner when this happens) after that when the request is finished, isSendingRequest is set to false and the component is rendered.
Hope it helps!
Please always add root path route "/" below of all other routes like:
<Route path="/checkout" component={YourComponent} />
.....
.....
<Route path="/" component={App} />

Resources