Setting up authentication flow in React applications

Setting up authentication flow in React applications

Ivan Mironchik - 26 August 2019

9 minutes read

In this article, we are going to learn how to setup user authentication with role based access in React applications.

tl;dr If you are impatient and want to go straight to the code, here is a link to the application with auth flow set up 😀

Global session container

First, let’s create the state container in order to keep the information regarding user authentication session globally.

// context.js

import React, { createContext, useState } from "react";
import { usePersistedState } from "./hooks";

export const userContext = createContext();

export const UserProvider = ({ children }) => {
  const [isAuthenticated, setAuthenticated] = usePersistedState(
    false,
    "isAuthenticated"
  );
  const [data, setData] = useState();

  const value = {
    data,
    isAuthenticated,
    setData,
    createSession: data => {
      setData(data);
      setAuthenticated(true);
    },
    removeSession: () => {
      setData(undefined);
      setAuthenticated(false);
    }
  }

  return (
    <userContext.Provider value={value}>
      {children}
    </userContext.Provider>
  );
};

Session state will be stored inside of React context, so we could access it before rendering of any page and make a decision whether the user is allowed to access it.

Once user logs in the application, we call createSession function, which will change isAuthenticated state to true to update the app accordingly. Along with setting state locally, we also keeping it in sync with local storage, to keep user authenticated across page reloads.

Note, we keep isAuthenticated flag instead of authentication token in the local storage in order not to be vulnerable to XSS attacks. That requires authentication token to be set via httpOnly cookies by a back-end server.

Along with keeping authentication state, we also set data variable for storing authorization data such as user role or his progress of the onboarding. That variable will tell us in the future which pages user is allowed to visit in our application.

Page component

Every application page will be wrapped into a Page component, which will load user data upon mounting and making a decision whether to grant or decline access depending on the user response.

// components/Page.js

import React, { useContext, useEffect } from "react";
import { Redirect } from "@reach/router";
import { userContext } from "../context";
import * as api from "../api";
import Loader from "./Loader";

export default ({ component: Component, guard, ...props }) => {
  const user = useContext(userContext);
  const page = <Component {...props} />;

  /**
   * When no guard provided
   * or user is not authenticated and guard allows - rendering the page
   */
  if (!guard || (!user.isAuthenticated && guard(false))) {
    return page;
  }

  /**
   * When user is not authenticated and guard denies - redirecting a user
   * to the page according to his user data
   */
  if (!user.isAuthenticated && !guard(false)) {
    return <Redirect noThrow to={redirectTo(false)} />;
  }

  /**
   * Otherwise rendering a component for Authenticated users
   */
  return (
    <Authenticated guard={guard} user={user}>
      {page}
    </Authenticated>
  );
};

const Authenticated = ({ children, guard, user }) => {
  const isDataLoaded = !!user.data;

  /**
   * Fetching user personal data at first render
   */
  useEffect(() => {
    if (!isDataLoaded) {
      api.fetchUser().then(user.setData);
    }
  }, [isDataLoaded, user]);

  if (!isDataLoaded) return <Loader />;

  /**
   * Once user is loaded, checking his ability to access the page
   * If denied - redirecting him to the page he has access to
   */
  if (!guard(true, user.data)) {
    return <Redirect noThrow to={redirectTo(true, user.data)} />;
  }

  /**
   * Otherwise user is allowed and we render page content
   */

  return children;
};

export const redirectTo = (isAuthenticated, userData) => {
  if (!isAuthenticated) {
    return "/sign-in";
  }

  if (!userData.firstName && !userData.lastName) {
    return "/profile-setup";
  }

  return "/";
};

Here, we are using the conception of guards, which essentially are simple functions taking isAuthenticated variable and user personal data as input and returning boolean value which indicates whether the user is allowed to access specific page. Depending on the application, there could be multiple guards for example, for unauthenticated users (which will allow access only to the sign in and sign up pages), onboarding users (will allow access to the onboarding frames but not the complete application), authenticated users (which will allow the access to the rest application)

Once guard denies an access to the page, we should do a redirect. The interesting point is, that depending on the user state we do different redirects. That’s what redirectTo function is responsible for. When signed out user tries to access the main page, it’s good to redirect him to the sign in page. When authenticated user accesses sign in page, which should move him back to the main application frame. When the user who not finished setting up his profile opens main page, we should move him to the profile setting page and so on.

Under the hood of Page component, at the first point we make our guard check across the signed out users, in the success case when guard approves without user data, we render page content, otherwise we do appropriate redirect.

If after all our guard still remains failed, we have to load the user personal data which will allow us to repeat guard check but with user data provided. That’s what Authenticated component is responsible for, which has similar logic to the main Page component in addition to fetching the user data itself. Finally if guards give us a green light - we’re good to render page content.

Integration with router

In this example we’ll be using @reach/router but a similar flow could be applied to the react-router or similar libraries.

In the place when we define our application routes, we have to wrap every component into a Page and providing a guard(if needed). In that case once the needed route loads, at the first point the logic of the page component will be executed and depending on access user will be either shown the content of the page or made a redirect to the allowed location.

// routes.js

import React from "react";
import { Router } from "@reach/router";
import Page from "./components/Page";
import SignIn from "./pages/sign-in";
import Main from "./pages/main";
import ProfileSetup from "./pages/profile-setup";
import * as guards from "./guards";

export default () => (
  <Router>
    <Page
      path="/sign-in"
      guard={guards.unauthenticated}
      component={SignIn}
    />
    <Page
      path="/profile-setup"
      guard={guards.profileNotSet}
      component={ProfileSetup}
    />
    <Page path="/" guard={guards.profileSet} component={Main} />
  </Router>
);

Real world application

In order to have a full picture of the flow, it’s good to illustrate that approach on a real-world scenario.

In our case we’ll have an application with the following pages:

  • Sign in. After success authentication, depending on whether the user set up his profile or not, he will be redirected either on the profile setup or main application page.
  • Profile setup. Frame for the users who didn’t finished the profile setup flow.
  • Main page. Frame where regular application users are living.

Here is the working example illustrating the complete flow described above.

Comments