AkThao
AkThao

Reputation: 643

Delay Auth0 redirect in React until after user credentials have been received

I'm working on a React app that uses auth0.js for user authentication.

Currently the process is:

  1. User clicks the Login button on the homepage, which redirects them to the login page, where they enter login details and submit the form.
  2. When the form is submitted, the user gets logged in with Auth0 (or errors are handled appropriately) and is redirected back to the homepage.
  3. As the app refreshes, a custom handleAuthentication() function is called, which parses the URL hash fragment and sets the user's session, which involves storing the access token, expiry time and user info in localStorage to provide persistence on refresh.
  4. Once back on the homepage, the Login button should be replaced with an avatar of the user's profile image and a link to their profile page.

However, while this process works, it doesn't always work the first time round. Sometimes I need to refresh the page because the user info cannot be found. I've discovered the reason is that the call to auth0client.client.userInfo() is asynchronous, so there is a slight delay in retrieving the user info to be stored in localStorage. Therefore, it's likely that the homepage will render before Auth0 sends a response. By the time I refresh the page a second time, the user info has been retrieved and stored, so the homepage renders properly.

The following is some of the relevant code in the above process:

auth.login()

login(realm, username, password, handleIncorrect) {
    //   Attempt to log the user in
    this.auth0Client.login(
      {
        realm: realm,
        username: username,
        password: password,
      },
      (err, authResult) => {
        // handle result/error
      }
    );
  }

auth.handleAuthentication()

handleAuthentication() {
    //   Attempt to authenticate the user in the app
    this.auth0Client.parseHash({}, (err, authResult) => {
      if (err) {
        return;
      } else if (authResult && authResult.accessToken && authResult.idToken) {
        this.setSession(authResult);
      }
    });
  }

auth.setSession()

setSession(authResult) {
    //   Use the results from handleAuthentication() to set up the user's browser session
    let expiresAt = authResult.expiresIn * 1000 + new Date().getTime();
    this.accessToken = authResult.accessToken;
    this.idToken = authResult.idToken;
    this.expiresAt = expiresAt;
    localStorage.setItem("access_token", authResult.accessToken);
    localStorage.setItem("id_token", authResult.idToken);
    localStorage.setItem("expires_at", expiresAt);
    this.auth0Client.client.userInfo(authResult.accessToken, (err, user) => {
      if (err) {
        console.log("setSession: Couldn't get user info");
        return;
      } else {
        localStorage.setItem("user", JSON.stringify(user));
      }
    });
  }

auth.getUserInfo()

getUserInfo() {
    //   Return user info
    const user = localStorage.getItem("user");
    if (!user) {
      console.log("getUserInfo: No user info to retrieve");
      return;
    }
    return JSON.parse(user);
  }

App.js

export default function App() {
  auth.handleAuthentication();

  return (
      <BrowserRouter>
        <Suspense fallback={<p>Loading...</p>}>
          <Layout>
            <Switch>
              // Routes to pages
            </Switch>
          </Layout>
        </Suspense>
      </BrowserRouter>

auth is a custom class I've created to extend the default Auth0 functionality by using localStorage, amongst other things.

App.js shows that handleAuthentication() (and by extension, setSession()) is called just before rendering the new route (homepage in this case). But setSession() often takes longer to complete than the page takes to render. So when the homepage loads and calls auth.getUserInfo(), the user info might initially be missing from localStorage, hence the user is undefined error that I get about half the time.

After doing some research, there a couple of possible solutions I came up with.

  1. Use a timeout to delay the homepage from rendering until the user info exists in localStorage and show a loading page in the meantime. But I read on other SO answers that you should not delay React from rendering as this can cause other problems. So solution #2:
  2. Initially display a placeholder avatar image on the homepage, until the user info is available in localStorage. Then, update the state with the user info to cause a re-render and display the user's actual avatar image.

Are either of these methods a good/correct approach? If not, what other ways are there that I can use to deal with this?

Upvotes: 1

Views: 1205

Answers (1)

Gabriel Lupu
Gabriel Lupu

Reputation: 1447

I would do something like this:

Use state to track when the response has arived const [arrived, setArrived] = useState(false)

and change it inside setSession() after the item is added to localStorage:

this.auth0Client.client.userInfo(authResult.accessToken, (err, user) => {
      if (err) {
        console.log("setSession: Couldn't get user info");
        return;
      } else {
        localStorage.setItem("user", JSON.stringify(user));
        // over here
        setArrived(true);
      }
    });

and run getUserInfo() inside an useEffect():

useEffect(() => {
    // run logic here to get the user from localStorage
    // I would also keep this information inside state
}, [arrived])

Upvotes: 1

Related Questions