© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
D. B. Duldulao, R. J. L. CabagnotPractical Enterprise Reacthttps://doi.org/10.1007/978-1-4842-6975-6_12

12. Protecting Routes and Authentication in React

Devlin Basilan Duldulao1   and Ruby Jane Leyva Cabagnot1
(1)
Oslo, Norway
 

In the last chapter, we’ve shown how to create, delete, and update our app’s events using Redux Toolkit. We’ve learned how efficient and convenient it is to do CRUD with our Store, which holds all our application’s global state.

In this chapter, we are going to build a login and a registration form for our application. We will start with the fake Node json-server, which we have installed in the previous chapter. The json-server allows us to send HTTP methods or HTTP requests.

Setting Up the Fake Server

Setting up the fake server only takes us a few minutes, and it’s a big help in building our UI; we don’t need to wait for our back-end dev team to give us the API. We can just create a fake API and use it to test out the UI.

So that’s what we’re doing in our json-server. We will also use the json-server-auth, a plugin or module, to create an authentication service inside the json-server.

Aside from the json-server-auth, we are using concurrently.

Concurrently allows us to run two npm commands simultaneously in one run.

So we will need to modify our scripts.

Go to the package.json , and edit the back-end script and add a start:fullstack script, as shown in Listing 12-1.
"backend": "json-server --watch db.json --port 5000 --delay=1000 -m ./node_modules/json-server-auth",
  "start:fullstack": "concurrently "npm run backend" "npm run start""
Listing 12-1

Modifying the Scripts in package.json

Concurrently runs multiple commands together or at the same time.

Now that we have it in setup, let’s give it a try. Cancel all the running applications, and then in the terminal, type the following command:
npm run start:fullstack

db.json

Once that’s done, let’s update the db.json . Below the events, we are going to add an array of the user object, as shown in Listing 12-2.
  "users": [
    {
      "id": "7fguyfte5",
      "email": "[email protected]",
      "password": "$2a$10$Pmk32D/fgkig8pU.r1rGrOpYYJSrnqqpLO6dRdo88iYxxIsl1sstC",
      "name": "Mok Kuh",
      "mobile": "+34782364823",
      "policy": true
    }
  ],
Listing 12-2

Adding the users Object in the db.json

We will use it later for logging in. The user’s endpoint is an array of the user’s object. It has the login details, including the hashed password.

API: Login and Register

Next, in the axios.ts file, let’s update the endpoints, as shown in Listing 12-3.
export const EndPoints = {
  sales: 'sales',
  products: 'products',
  events: 'events',
  login: 'login',
  register: 'register',
};
Listing 12-3

Updating the Endpoints in axios.ts

Both the “login” and “register” are part of the json-server-auth. If you go to npmjs.org and search for json-server-auth, you’ll see that we can use any of the following routes in the authentication flow.

In this case, we went with login and register, as shown in Figure 12-1.
../images/506956_1_En_12_Chapter/506956_1_En_12_Fig1_HTML.jpg
Figure 12-1

Authentication flow in json-server-auth

authService

We can now update the service. In the services folder, create a new file called authService.ts .

The authService is a file that contains our logging and registering services with the use of axios.
import axios, { EndPoints } from 'api/axios';
export type UserModel = {
  email: string;
  password: string;
};
/*The return object will be an object with an access token of type string. We're expecting an access token from the json-server-auth */
export async function loginAxios(userModel: UserModel) {
  return await axios.post<{ accessToken: string }>(EndPoints.login, userModel);
}
export type RegisterModel = {
  email: string;
  password: string;
  name: string;
  mobile: string;
  policy: boolean;
};
export async function registerAxios(registerModel: RegisterModel) {
  return await axios.post<{ accessToken: string }>(
    EndPoints.register,
    registerModel,
  );
}
Listing 12-4

Creating the authService.ts

In Listing 12-4, we have the loginAxios – requests a userModel – and we’re defining it in the type UserModel, which requires an email and password of type string. We also have the registerAxios – requests a registerModel – and we’re describing it in the RegisterModel, which requires email, password, name, mobile, and policy.

Let’s move on now to the creation of the Login page.

Inside the views ➤ pages folder, create a new folder and name it auth, and inside the auth, add another folder and name it components.

In the auth folder, create a new file and name it LoginPage.tsx:
app ➤ views ➤ pages ➤ auth ➤ LoginPage.tsx
In the components folder, create a new file and name it LoginForm.tsx:
app ➤ views ➤ pages ➤ auth ➤ components ➤ LoginForm.tsx

Setting Up the Login Form

Let’s build up first the LoginForm. Import the named components, as shown in Listing 12-5.
import React, { useState } from 'react';
import * as Yup from 'yup';
import { Formik } from 'formik';
import { Alert } from '@material-ui/lab';
import { useHistory } from 'react-router-dom';
import {
  Box,
  Button,
  FormHelperText,
  TextField,
  CardHeader,
  Divider,
  Card,
} from '@material-ui/core';
import { loginAxios } from 'services/authService';
Listing 12-5

Importing Named Components of LoginForm.tsx

We have the usual suspects of named imports in Listing 12-5. Let’s see what else we have here that is new:

Alert: We’re importing Alert from Material-UI for the first time here. Alert is used to display a short and important message to get the user’s attention without interrupting the user’s task.

useHistory: This is a Hook that we import from the React-Router-DOM; this allows us to access the history instance and lets us navigate back.

We also imported the loginAxios from the authService.

Then let’s create the function for the LoginForm and another function to save the user’s auth details. And, of course, we will name it as such, as shown in Listing 12-6.

As a best practice, we should name our functions and methods as descriptively as possible to make it easy on ourselves and other developers reading our code.
const LoginForm = () => {
  const key = 'token';
  const history = useHistory();
  const [error, setError] = useState('');
  const saveUserAuthDetails = (data: { accessToken: string }) => {
    localStorage.setItem(key, data.accessToken);
  };
Listing 12-6

Creating the Function for LoginForm.tsx

LoginForm: In here, we’re defining the “token” as the key and using the useHistory and the useState for the error.

saveUserAuthDetails: A function that saves users’ details in the local storage. The local storage is a native part of the browser, so we have access to it. It’s a first-class citizen supported, so we don’t need to import it anymore.

Next, let’s add the return statement of our LoginForm, which contains Formik and its required props, as shown in Listing 12-7.
return (
    <Formik
      initialValues={{
        email: '[email protected]',
        password: 'Pass123!',
      }}
      validationSchema={Yup.object().shape({
        email: Yup.string()
          .email('Must be a valid email')
          .max(255)
          .required('Email is required'),
        password: Yup.string().max(255).required('Password is required'),
      })}
      onSubmit={async (values, formikHelpers) => {
        try {
          const { data } = await loginAxios(values);
          saveUserAuthDetails(data);
          formikHelpers.resetForm();
          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          history.push('dashboard');
        } catch (e) {
          setError('Failed. Please try again.');
          console.log(e.message);
          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >
      {/* deconstructed Formik props */}
      {({
        errors,
        handleBlur,
        handleChange,
        handleSubmit,
        isSubmitting,
        touched,
        values,
      }) => (
        <Card>
          <form noValidate onSubmit={handleSubmit}>
            <CardHeader title="Login" />
            <Divider />
            <Box m={2}>
              <TextField
                error={Boolean(touched.email && errors.email)}
                fullWidth
                autoFocus
                helperText={touched.email && errors.email}
                label="Email Address"
                margin="normal"
                name="email"
                onBlur={handleBlur}
                onChange={handleChange}
                type="email"
                value={values.email}
                variant="outlined"
              />
              <TextField
                error={Boolean(touched.password && errors.password)}
                fullWidth
                helperText={touched.password && errors.password}
                label="Password"
                margin="normal"
                name="password"
                onBlur={handleBlur}
                onChange={handleChange}
                type="password"
                value={values.password}
                variant="outlined"
              />
              <Box mt={2}>
                <Button
                  color="primary"
                  disabled={isSubmitting}
                  fullWidth
                  size="large"
                  type="submit"
                  variant="contained"
                >
                  Log In
                </Button>
              </Box>
              {error && (
                <Box mt={3}>
                  <FormHelperText error>{error}</FormHelperText>
                </Box>
              )}
              <Box mt={2}
                <Alert severity="info">
                  <div>
                    Use <b>[email protected]</b> and password <b>Pass123!</b>
                  </div>
                </Alert>
              </Box>
            </Box>
          </form>
        </Card>
      )}
    </Formik>
  );
};
export default LoginForm;
Listing 12-7

Creating Formik in the LoginForm

Let’s review some of what we have done here in Listing 12-7:

initialValues: A required prop of Formik. We are initializing it with the value of email and password.

validationSchema: A Yup validation schema. We’re defining the email as a string with a valid email address with a max character of 255 and the password a string with a max character of 255.

onSubmit: An async function that takes on values and formikHelpers. Since it’s an async function, we are wrapping it in a try-catch block.

Inside the try, we’re using the loginAxios to see if we can log in. The result we need is just this data, which is a destructuring of a big object result. We don’t need to get all the properties of this enormous object.

We then save the data to the saveUserAuthDetails, which means keeping it in our local storage.

Then we have a set of formikHelpers such as resetForm, setStatus, and setSubmitting that we are using.

For the catch, we put the setError in case of an unsuccessful login.

We’re using the Card component from Material-UI to style the login UI, and we are using two TextFields, one each for the Email and the Password.

Creating a Register Form

After that, we’ll need to create another component under the auth ➤ components folder. Let’s name it RegisterForm .tsx.

Again, let’s do the named components first, as shown in Listing 12-8.
import React, { useState } from 'react';
import * as Yup from 'yup';
import { Formik } from 'formik';
import { Alert } from '@material-ui/lab';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Checkbox,
  CircularProgress,
  Divider,
  FormHelperText,
  Grid,
  Link,
  TextField,
  Typography,
} from '@material-ui/core';
import { useHistory } from 'react-router-dom';
import { registerAxios } from 'services/authService';
Listing 12-8

Importing Named Components in RegisterForm.tsx

The Register Form needs the same as the Login except for a few more modules from Material-UI. We’re also adding the registerAxios from authService.

Next, let’s create the functions to register the user and save their auth details in the local storage, as shown in Listing 12-9.
const RegisterForm = () => {
  const key = 'token';
  const history = useHistory();
  const [error, setError] = useState('');
  const [isAlertVisible, setAlertVisible] = useState(false);
  const saveUserAuthDetails = (data: { accessToken: string }) => {
    localStorage.setItem(key, data.accessToken);
  };
Listing 12-9

Adding the RegisterForm Function

And the return statement wrapped in Formik, as shown in Listing 12-10.
return
    <Formik
      initialValues={{
        email: '[email protected]',
        name: 'John',
        mobile: '+34782364823',
        password: 'Pass123!',
        policy: false,
      }}
      validationSchema={Yup.object().shape({
        email: Yup.string().email().required('Required'),
        name: Yup.string().required('Required'),
        mobile: Yup.string().min(10).required('Required'),
        password: Yup.string()
          .min(7, 'Must be at least 7 characters')
          .max(255)
          .required('Required'),policy: Yup.boolean().oneOf([true], 'This field must be checked'),
      })}
      onSubmit={async (values, formikHelpers) => {
        try {
          const { data } = await registerAxios(values);
          saveUserAuthDetails(data);
          formikHelpers.resetForm();
          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          history.push('dashboard');
        } catch (e) {
          setError(e);
          setAlertVisible(true);
          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >
      {({
          errors,
          handleBlur,
          handleChange,
          handleSubmit,
          isSubmitting,
          touched,
          values,
        }) => (
        <Card>
          <CardHeader title="Register Form" />
          <Divider />
          <CardContent>
            {isAlertVisible && (
              <Box mb={3}>
 <Alert onClose={() => setAlertVisible(false)} severity="info">{error}!
 </Alert>
              </Box>
            )}
            {isSubmitting ? (
     <Box display="flex" justifyContent="center" my={5}>
          <CircularProgress />
       {/*for the loading spinner*/}
              </Box>
            ) : (
              <Box>
                <Grid container spacing={2}>
                  <Grid item md={6} xs={12}>
                    <TextField
            error={Boolean(touched.name && errors.name)}
                      fullWidth
            helperText={touched.name && errors.name}
                      label="Name"
                      name="name"
                      onBlur={handleBlur}
                      onChange={handleChange}
                      value={values.name}
                      variant="outlined"
                    />
                  </Grid>
                  <Grid item md={6} xs={12}>
                    <TextField
        error={Boolean(touched.mobile && errors.mobile)}
                      fullWidth
         helperText={touched.mobile && errors.mobile}
                      label="Mobile"
                      name="mobile"
                      onBlur={handleBlur}
                      onChange={handleChange}
                      value={values.mobile}
                      variant="outlined"
                    />
                  </Grid>
                </Grid>
                <Box mt={2}>
                  <TextField
          error={Boolean(touched.email && errors.email)}
                    fullWidth
              helperText={touched.email && errors.email}
                    label="Email Address"
                    name="email"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    type="email"
                    value={values.email}
                    variant="outlined"
                  />
                </Box>
                <Box mt={2}>
                  <TextField
    error={Boolean(touched.password && errors.password)}
                    fullWidth
     helperText={touched.password && errors.password}
                    label="Password"
                    name="password"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    type="password"
                    value={values.password}
                    variant="outlined"
                  />
                </Box>
 <Box alignItems="center" display="flex" mt={2} ml={-1}>
                  <Checkbox
                    checked={values.policy}
                    name="policy"
                    onChange={handleChange}
                  />
      <Typography variant="body2" color="textSecondary">
                    I have read the{' '}
         <Link component="a" href="#" color="secondary">
                      Terms and Conditions
                    </Link>
                  </Typography>
                </Box>
           {Boolean(touched.policy && errors.policy) && (
                  <FormHelperText error>{errors.policy}</FormHelperText>
                )}
                <form onSubmit={handleSubmit}>
                  <Button
                    color="primary"
                    disabled={isSubmitting}
                    fullWidth
                    size="large"
                    type="submit"
                    variant="contained"
                  >
                    Sign up
                  </Button>
                </form>
              </Box>
            )}
          </CardContent>
        </Card>
      )}
    </Formik>
  );
};
export default RegisterForm;
Listing 12-10

Creating Formik in the RegisterForm.tsx

In the initialValues, you can leave it an empty string or pass an example value. Note that we don’t save or store the password here. We are just doing it for demo purposes.

Also, the initialValues and validationSchema are typically saved in a separate file for a cleaner code, especially a long file.

So that’s it for now in the Register Form; we’ll test it out later. Let’s build up the Login page now.

Adding the Login Page

Let’s now create the LoginPage, and we’ll start with importing the named components that we need, as shown in Listing 12-11.
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box, Button, Container, Divider } from '@material-ui/core';
import LoginForm from './components/LoginForm';
import RegisterForm from './components/RegisterForm';
import Page from 'app/components/page';
Listing 12-11

Importing the Named Components in LoginPage.tsx

We imported the LoginForm and RegisterForm that we’ve just created. We also have the Page template.

So, next, let’s create the LoginPage component function, as shown in Listing 12-12.
  const LoginPage = () => {
  const classes = useStyles();
  const [isLogin, setIsLogin] = useState(true);
Listing 12-12

Creating the LoginPage Function

isLogin: A local state, and it’s a Boolean that is set to true by default because we’re logging in. If this is false, we are showing the Register Form. How do we do that? By doing this: {isLogin ? <LoginForm /> : <RegisterForm />}.

So now, let’s make that return statement next, as shown in Listing 12-13.
return (
    <Page className={classes.root} title="Authentication">
      <Container>
        <Box
          my={5}
          display={'flex'}
          flexDirection={'column'}
          justifyContent={'center'}
          alignItems={'center'}
        >
          {/*if isLogin is true - show LoginForm, otherwise show RegisterForm */}
          {isLogin ? <LoginForm /> : <RegisterForm />}
          <Divider />
          <Box mt={5}>
            Go to{' '}
            {isLogin ? (
              <Button
                size={'small'}
                color={'primary'}
                variant={'text'}
                onClick={() => setIsLogin(false)}
              >
                Register Form
              </Button>
            ) : (
              <Button
                size={'small'}
                color={'primary'}
                variant={'text'}
                onClick={() => setIsLogin(true)}
              >
                Login Form
              </Button>
            )}
          </Box>
        </Box>
      </Container>
    </Page>
  );
};
const useStyles = makeStyles(() => ({root: {},}));
export default LoginPage;
Listing 12-13

Adding the Return Statement of the LoginPage.tsx

Okay, that’s done for now. Time to update the routes.tsx.

Updating the Routes

Below the AboutPage routes, insert the LoginPage routes, as shown in Listing 12-14.
<Route
     exact
     path={'/login'}
     component={lazy(() => import('./views/pages/auth/LoginPage'))}
        />
Listing 12-14

Adding the LoginPage Routes

Let’s test it in our browser. Click the refresh button or go to your localhost:3000/login, and you should see the Login page, as shown in Figure  12-2.
../images/506956_1_En_12_Chapter/506956_1_En_12_Fig2_HTML.jpg
Figure 12-2

Screenshot of the Login page

Click the Register Form and create your account, as shown in Figure 12-3.
../images/506956_1_En_12_Chapter/506956_1_En_12_Fig3_HTML.jpg
Figure 12-3

Screenshot of the Register Form

To check for a successful login or sign-up, go to Network of Chrome DevTools, and you should see under the Headers Status Code OK, and in the Response, you’ll see the access token.

Copy the access token, and let’s see what’s inside. And for that, we will go to this excellent site jwt.io and paste our access token there.

JSON Web Token (JWT)

The JWT has the Header, Payload, and Signature. In the Header, you’ll see the alg or algorithm and the typ or type. In the Payload, data are the decoded values of the access token or the JWT. The decoded values are what we need inside our React app in the next chapter.
../images/506956_1_En_12_Chapter/506956_1_En_12_Fig4_HTML.jpg
Figure 12-4

Checking the access token response from the server

Learn more about the JSON Web Token structure here at jwt.io/introduction.

Simply, JSON Web Tokens or JWTs are token-based authentications used to get access to a resource. Token-based authentication, which is stateless, is different from session-based authentication, which is stateful and requires a cookie with the session ID placed in the user’s browser.

This is a vast topic, so I urge you to read more about it.

Let’s go back to our app’s Chrome DevTools and click Application ➤ Local Storage and the localhost.
../images/506956_1_En_12_Chapter/506956_1_En_12_Fig5_HTML.jpg
Figure 12-5

Screenshot of the token being stored in the local storage

The token indicates that we’ve successfully stored the JWT or the JSON Web Token in our local storage.

Creating a ProtectedRoute Component

Next, we need to protect our routes so that an unauthenticated user cannot access or see the dashboard. To do this, let’s create a protected route.

Inside the app directory, go to components, and inside that, create a new component and name it protected-route.tsx:
app ➤ components ➤ protected-route.tsx
Open the protected-route.tsx file and copy the following code, as shown in Listing 12-15.
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
const ProtectedRoute = props => {
  const token = localStorage.getItem('token');
  return token ? (
    <Route {...props} />
  ) : (
    <Redirect to={{ pathname: '/login' }} />
  );
};
export default ProtectedRoute;
Listing 12-15

Creating the protected-route.tsx

In Listing 12-15, we keep it simple for now, but we will update it later as the authentication gets more complicated.

We imported the Redirect and Route from the React-Router-DOM. And we have the ProtectedRoute – a function that takes props and retrieves the user token from the localStorage.

In the return statement, we’re checking if there’s an existing token ? If this is true, the user is directed to a specific path inside the dashboard; otherwise, the user is redirected to the Login page.

After doing that, we can now use the ProtectedRoute component to wrap the dashboard route.

Updating the Routes.tsx

Go to routes.tsx, and we will use the ProtectedRoute component, as shown in Listing 12-16.
import ProtectedRoute from './components/protected-route';
...
<ProtectedRoute
          path={'/dashboard'}
          render={({ match: { path } }) => (
            <Dashboard>
              <Switch>
                <Route
                  exact
                  path={path + '/'}
                  component={lazy(() => import('./views/dashboard/dashboard-default-content'),)/>
Listing 12-16

Adding the ProtectedRoute in the routes.tsx

Check if it’s working. Open a new window and go to localhost:3000/dashboard. Since the token is already in our local storage, we can access the dashboard right away without being redirected to the Login page.

Updating the Dashboard Sidebar Navigation

After this, we will need to update the logout.

Go to the dashboard-layoutdashboard-sidebar-navigation .tsx.

We will create a new handle event function for the logout. Put it just below the handleClick function, as shown in Listing 12-17.
const handleLogout = () => {
    localStorage.clear();
  };
Listing 12-17

Updating the dashboard-sidebar-navigation.tsx

handleLogout: A function that is clearing the localStorage by removing all the stored values

Inside the same file, go to the logout, and let’s add an onClick event on the button so we can trigger it, as shown in Listing 12-18.
<ListItem button onClick={handleLogout}>
                  <ListItemIcon>
                    <LogOutIcon />
                  </ListItemIcon>
                  <ListItemText primary={'logout'} />
                </ListItem>
Listing 12-18

Adding an onClick Event for the handleLogout

Let’s test it.

Time to Test

Go to the dashboard and also open your Chrome DevTools and click Application.

Click the logout button, and the browser should refresh, and you’ll be directed to the main page; and if you look at the Chrome DevTools, the token should get deleted.
../images/506956_1_En_12_Chapter/506956_1_En_12_Fig6_HTML.jpg
Figure 12-6

Screenshot of the LocalStorage after the token was deleted after logout

So this is how we can create a simple authentication.

We will be improving on this in the coming chapters as our app gets complicated.

../images/506956_1_En_12_Chapter/506956_1_En_12_Figa_HTML.jpg I want to emphasize that it’s vital that we know the basics of authentication and how it works. But these days, to be honest, I would highly recommend authentication as a service or an identity provider.

Some reasons why I would recommend a third-party identity as a service:
  1. 1.

    Decentralizing the identity from your applications. The user’s identity info will not be stored in your database.

     
  2. 2.

    Allows developers to focus on developing the application’s business value instead of working for weeks building the authentication and authorization service.

     
  3. 3.

    Most third-party identity as a service companies such as Auth0, which I also personally use and recommend, are very secure and reliable. Auth0 also has good documentation, lots of open source projects that you can build on, and a strong community support.

    Other excellent identity as a service providers that I’ve tried are AWS Cognito, Azure AD, and Okta. Many of them offer a free tier program, which is best for small projects, so you can understand how it works.

     
(Full disclosure: I'm currently an Auth0 Ambassador. No, I'm not an employee of the company, nor do I get any monetary compensation. I occasionally get some swags and other excellent perks whenever I speak at a conference and mention them. BUT I became an Auth0 Ambassador precisely because I've been using it and recommending them even before.)
  1. 4.

    Lastly, these third-party identity providers are being developed and maintained by security engineers or those specializing in security. They are more updated on what’s going on in the world of cybersecurity, including the best practice, trends, and issues.

     

Summary

This chapter built the Login and Registration Forms with the styling help from the Material-UI components. We used the library json-server-auth inside our pseudo Node json-server to simulate the implementation flow of authenticating and protecting our routes. We also made it easy for ourselves to run and build our app when we added concurrently in our scripts.

In the next chapter, we will be building more components and functionalities in our React application. We begin with creating a Profile Form and then syncing it to different deep layer components in our app – all with the powerful help of Redux.

Lastly, we will show that not all of our components need to be tied up to Redux. If we don’t need the added complexity, then it does not make sense to use it. Some people have this mistaken notion that once we add Redux to our app, all our components must include it. Use it if you need it; otherwise, you don’t have to.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.8.247