2
votes

I'm currently developing an application using the MERN stack with Passport.js for authentication, specifically Google OAuth2 Strategy. My current application has simple login functionalities that redirects from client to the Google consent page and back to the server's redirect url, which should then redirect to client's main app. I'm pretty sure that's all you need to do in the server side and now I'm slightly confused as to how to handle this on the React Router side.

So first, I have my Google Sign-in button in my Public route which then hits the endpoint in the server that triggers the Google OAuth

class Public extends Component {
    render() {
        return (
            <div>
                <h1>Public Route</h1>
                <a href="http://localhost:5000/api/auth/google">Sign In</a>
            </div>
        )
    }
}

After the server redirects from localhost:5000/auth/google/redirect to localhost:3000/protected, I believe it goes back to App.js that triggers the React router

class App extends Component {

    constructor(props) {

        super(props)
        this.state = { authenticated: false }
    }

    componentDidMount() {

        api.get('/auth/user').then(({data}) => {

            if (data) this.setState({authenticated: true})
        }).catch((err) => console.error(err))
    }

    render() {

        const { authenticated } = this.state
        return (
            <div className="App">
                <Switch>
                    <Route exact path='/' component={Public} />
                    <PrivateRoute exact authed={authenticated} path='/app' component={Protected} />
                </Switch>
            </div>
        )
    }
}

and this this is where things get a little messy. So I think it's going to render first before componentDidMount(), so authenticated state is still false so PrivateRoute redirects the client back to localhost:3000/ and renders the Public route. And then after all that, runs componentDidMount() and finally fetches the user and now authenticated is true, but it's too late now since the client was already redirected to localhost:3000/ so it's not hitting the PrivateRoute anymore.

Here's my PrivateRoute component in case anyone was wondering:

const PrivateRoute = ({ component: Component, authed, ...rest }) => (
    <Route { ...rest} render={(props) => authed ? 
        <Component {...props} /> :
        <Redirect to={{pathname: '/'}} />
    } />
)

What I've done so far:

  • use componentWillMount() - no difference, plus I believe this will get deprecated soon
  • use a global auth object which has an isAuthenticated, login(), and logout() members. I created an instance of that object in my Public component and added onClick={auth.login} along with the href. However, onClick runs first before href browser redirect, so it tries to fetch the user first (even though there isn't any set yet), so isAuthenticated doesn't get set to true

Thanks in advance!

1

1 Answers

0
votes

I think it would be easier to take care of authentication checks within your privateRoute component itself instead of splitting up across App and privateRoute.

Regardless, instead of redirecting when the user is not authenticated, you could render a 'loading screen' component that will still block them from reaching the protected route. This way, you can wait for the http request to return before making a final decision about whether or not to allow user access or to redirect them back to the login page. Here is an example:

const PrivateRoute = ({ component: Component, ...rest }) => {
    const [isAuthenticated, setAuthenticated] = useState(false);

    useEffect(() => {
        axiosInstance.get('/auth/isauthenticated').then(({ data: { user } }) => {
            if (user) {
                setAuthenticated(true);
            } else {
                window.location = '/menu/login';
            }
        });
    }, []);

    return (isAuthenticated 
              ? <Route {...rest} render={(props) => <Component {...props} />} /> 
              : <p>{'Loading'}</p>;)
}