0

I have a React app that connects to an API. I'm using React-Router and I'm trying to implement authentication check to redirect the user to the login page if not authenticated with the API. Here is the routing part of App.js:

  ...

  render() {
    return (
      <div className="Margins">
        <BurgerMenu
          isAuthenticated={this.state.isAuthenticated}
          logoutCallback={this.logoutCallback}
        />
        <Routes>
          <Route
            path="/"
            element={
              <AuthenticationRequired
                isAuthenticated={this.state.isAuthenticated}
                setAuthenticationCallback={this.setAuthenticationCallback}
              >
                <Home />
              </AuthenticationRequired>
            }
          />
          <Route
            path="/product"
            element={
              <AuthenticationRequired
                isAuthenticated={this.state.isAuthenticated}
                setAuthenticationCallback={this.setAuthenticationCallback}
              >
                <Product />
              </AuthenticationRequired>
            }
          />
          <Route
            path="/login"
            element={
              <Login
                setTokenCallback={this.setTokenCallback}
                isAuthenticated={this.state.isAuthenticated}
              />
            }
          />
          <Route path="/contact" element={<Contact />} />
          <Route path="/register" element={<Register />} />
        </Routes>
      </div>
    );
  }
}

export default App;

There are two authenticated endpoints: "/" and "/product". The AuthenticationRequired component is as follow:

import {Component} from "react";
import { Navigate } from "react-router-dom";
import axios from "axios";
let config = require("../config.json");

class AuthenticationRequired extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isAuthenticated: props.isAuthenticated,
      setAuthentication: props.setAuthenticationCallback
    };
  }

  checkAuthentication() {
    if (this.props.isAuthenticated) return;
    console.log("check authentication")
    axios.get(config.AUTHENTICATION_CHECK_URL,
      { withCredentials: true }
    )
      .then((response) => {
        this.props.setAuthenticationCallback(true);
      })
      .catch((error) => {
        this.props.setAuthenticationCallback(false);
      })
  }

  render() {
    this.checkAuthentication();
    if (this.props.isAuthenticated) return this.props.children;
    return <Navigate to="/login" replace />;
  }
}

export default AuthenticationRequired;

So it basically checks if the user is authenticated by making an API call. If the session cookie is valid, the servers sends a 200 response code and the user is allowed to access the route. When connecting to the "/" route, it works as expected. However, when trying to access "/product", the .then function never gets executed so I end up being redirected to the login page. I checked in the network tab of my browser and a get request is being sent to the server which replies with a 200 status code so I would expect the .then to be executed but it's not. I don't understand what's going on here.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • There is no `componentDidMount` lifecycle method in any of the code you've shared. The `AuthenticationRequired` is also incorrectly calling `checkAuthentication` as an unintentional side-effect during the render cycle. Please [edit] the post to include a complete [mcve]. – Drew Reese Jul 05 '23 at 17:32
  • I don't think `AuthenticationRequired` is calling `checkAuthentication` as an unintentional side-effect. Rather, it is calling it as a deliberate way to verify the user's authentication status before rendering the children components or redirecting to the login page. This is similar to how some examples of authentication with `React Router v6` use the `useLayoutEffect` hook to perform the same check. However, if you think that this approach is problematic or inefficient, you can suggest a better alternative or explain why you think so. I'm always open to learning new things. – Pluto Jul 05 '23 at 17:49
  • @VictorL. Is that comment directed to me? If so, then yes, it absolutely is an unintentional side-effect as the `render` method is to be considered a ***pure*** function. If you need to issue ***intentional*** side-effects then use the appropriate lifecycle method(s) to do so, e.g. `componentDidMount`, `componentDidUpdate`, and/or `componentWillUnmount`. The `useEffect` (*and `useLayoutEffect`*) hook is the spiritual successor to these methods in React function components. The OP's approach is problematic, which is why I asked for a complete [mcve] so we can see overall what they want to do. – Drew Reese Jul 05 '23 at 18:10
  • Initially, I had the call to checkAuthentication in the componentDidMount but that didn't work either. componentDidMount is called after the rendering is done. I want to check for authentication before rendering. – Francois Gosselin Jul 05 '23 at 18:40
  • @DrewReese, I didn't have understood your comment. You're correct that there is no componentDidMount lifecycle method in the shared code. So I agree that the OP's approach is problematic. Thank you for your explanation. – Pluto Jul 05 '23 at 19:38
  • Francois, that is generally how the React component lifecycle functions. See if my answer [here](/a/66289280/8690857) helps explain this process and route protection in general. If you still need help here can you [edit] to include a complete [mcve]? – Drew Reese Jul 05 '23 at 20:13
  • @DrewReese, Thank you for that. I ended up following a similar approach, adding a state variable to track the status of the authentication check and rendering a loading message while waiting for the response. – Francois Gosselin Jul 06 '23 at 13:59

1 Answers1

0

I found the problem. The response to the axios query was coming in after the redirection to the login page had happened. To make the render function wait until the response was received, I added a variable called authVerified to the state variables. This variable is set to false in the constructor and to true when the authentication status is updated. Then, in the render function, I check for authVerified and if false, I render a message saying the page is loading.

import {Component} from "react";
import { Navigate } from "react-router-dom";
import axios from "axios";
let config = require("../config.json");

class AuthenticationRequired extends Component
{
    constructor(props) {
        super(props);
        this.state = {
            isAuthenticated: props.isAuthenticated,
            setAuthentication: props.setAuthenticationCallback,
            authVerified: false
        };
    }
    
    componentDidMount() {
        axios.get(config.AUTHENTICATION_CHECK_URL, {withCredentials: true})
            .then((response) => {
                this.props.setAuthenticationCallback(true);
                this.setState({authVerified: true})
            })
            .catch(() => {
                this.props.setAuthenticationCallback(false);
                this.setState({authVerified: true});
            });
    }

    render(){
        console.log("rendering");
        if (!this.state.authVerified) return (<>Loading...</>)
        if (this.props.isAuthenticated) return this.props.children;
        return <Navigate to="/login" replace />;
    }



}

export default AuthenticationRequired;