Setting up authentication flow in React applications.

Ivan Mironchik
August 26th, 2019 · 3 min 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.

1// context.js
2
3import React, { createContext, useState } from "react";
4import { usePersistedState } from "./hooks";
5
6export const userContext = createContext();
7
8export const UserProvider = ({ children }) => {
9 const [isAuthenticated, setAuthenticated] = usePersistedState(
10 false,
11 "isAuthenticated"
12 );
13 const [data, setData] = useState();
14
15 const value = {
16 data,
17 isAuthenticated,
18 setData,
19 createSession: data => {
20 setData(data);
21 setAuthenticated(true);
22 },
23 removeSession: () => {
24 setData(undefined);
25 setAuthenticated(false);
26 }
27 }
28
29 return (
30 <userContext.Provider value={value}>
31 {children}
32 </userContext.Provider>
33 );
34};

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.

1// components/Page.js
2
3import React, { useContext, useEffect } from "react";
4import { Redirect } from "@reach/router";
5import { userContext } from "../context";
6import * as api from "../api";
7import Loader from "./Loader";
8
9export default ({ component: Component, guard, ...props }) => {
10 const user = useContext(userContext);
11 const page = <Component {...props} />;
12
13 /**
14 * When no guard provided
15 * or user is not authenticated and guard allows - rendering the page
16 */
17 if (!guard || (!user.isAuthenticated && guard(false))) {
18 return page;
19 }
20
21 /**
22 * When user is not authenticated and guard denies - redirecting a user
23 * to the page according to his user data
24 */
25 if (!user.isAuthenticated && !guard(false)) {
26 return <Redirect noThrow to={redirectTo(false)} />;
27 }
28
29 /**
30 * Otherwise rendering a component for Authenticated users
31 */
32 return (
33 <Authenticated guard={guard} user={user}>
34 {page}
35 </Authenticated>
36 );
37};
38
39const Authenticated = ({ children, guard, user }) => {
40 const isDataLoaded = !!user.data;
41
42 /**
43 * Fetching user personal data at first render
44 */
45 useEffect(() => {
46 if (!isDataLoaded) {
47 api.fetchUser().then(user.setData);
48 }
49 }, [isDataLoaded, user]);
50
51 if (!isDataLoaded) return <Loader />;
52
53 /**
54 * Once user is loaded, checking his ability to access the page
55 * If denied - redirecting him to the page he has access to
56 */
57 if (!guard(true, user.data)) {
58 return <Redirect noThrow to={redirectTo(true, user.data)} />;
59 }
60
61 /**
62 * Otherwise user is allowed and we render page content
63 */
64
65 return children;
66};
67
68export const redirectTo = (isAuthenticated, userData) => {
69 if (!isAuthenticated) {
70 return "/sign-in";
71 }
72
73 if (!userData.firstName && !userData.lastName) {
74 return "/profile-setup";
75 }
76
77 return "/";
78};

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.

1// routes.js
2
3import React from "react";
4import { Router } from "@reach/router";
5import Page from "./components/Page";
6import SignIn from "./pages/sign-in";
7import Main from "./pages/main";
8import ProfileSetup from "./pages/profile-setup";
9import * as guards from "./guards";
10
11export default () => (
12 <Router>
13 <Page
14 path="/sign-in"
15 guard={guards.unauthenticated}
16 component={SignIn}
17 />
18 <Page
19 path="/profile-setup"
20 guard={guards.profileNotSet}
21 component={ProfileSetup}
22 />
23 <Page path="/" guard={guards.profileSet} component={Main} />
24 </Router>
25);

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.

More articles from Obytes

Image resizing on the fly with AWS Lambda, API Gateway, and S3 Storage

How to use AWS Lambda, API Gateway and AWS S3 Storage to build an image resizing on the fly service.

August 20th, 2019 · 3 min read

Terraform remote state for collaboration.

Terraform remote state for collaboration.

August 20th, 2019 · 3 min read

ABOUT US

Our mission and ambition is to challenge the status quo, by doing things differently we nurture our love for craft and technology allowing us to create the unexpected.