Adding JWT Login Authentication in a React App

afrozansenjuti

Afroza Nowshin

Posted on August 28, 2022

Adding JWT Login Authentication in a React App

In recent times, one of the robust ways to authenticate a login credential is JWT authentication. Today, we will discuss how we can implement JWT authentication for a login app in React. For styling, I am using Material UI for React. Originally, I implemented JWT authentication over a private login API; replace this API with your private API.

First thing first, what is a JWT? JWT is short for JSON Web Token, and the JWT authentication is a compact way that ensures the safe transmission of information between parties as a JSON object. The data is verifiable because it is digitally signed using an HMAC algorithm (Hash-based Message Authentication Code).

This is how JWT authentication works :

JWT

What is happening here is, that when you sign up to a system for the first time, your password is encrypted with an HMAC algorithm, and therefore, it is hashed. After that, if you sign in, two types of token are generated against your password, both of which will expire after some time. This time is set by the back-end and for this reason, you need to save these tokens. After that, if you try to make any API request, you will send the tokens with the header. If your header format is invalid, so is your request. If you have a valid header, both the access and refresh tokens are checked.
If both the tokens are invalid, you force a logout action on whoever is trying to send that API request.

The steps that you need to follow to implement a JWT authentication in your front-end are as follows:

  1. Create a React App, then create a Login component with user input and a submit button and call it from App.js.
  2. Write a handleSubmit where you will save the user input as states and also the access and refresh tokens in the localStorage.
  3. Send an API request. Check if there is a header, if not this is an invalid request. If there is a header, check for access and refresh token and match with the values of the localStorage.
  4. Conditionally check status codes and hit Refresh API and Logout API if Needed
  5. If both the tokens are valid after checking all these conditions, you will receive a response.

Now that you understand how JWT works, let's code this workflow.

Step-1: Creating a React App and a Login Component

For this tutorial, I am using node version 17 and the latest version of yarn package manager. Create a react app named jwt and got to the folder jwt:

npx create-react-app jwt
cd jwt/

Enter fullscreen mode Exit fullscreen mode

You will write a component named Login.js. For styling, install Material-UI with yarn:

yarn add @material-ui/core
yarn add @mui/icons-material
Enter fullscreen mode Exit fullscreen mode

Inside Login component, you will be needing two text boxes for username and password, and a button to submit.


import React, { useState } from "react";
import Button from "@material-ui/core/Button";
import CssBaseline from "@material-ui/core/CssBaseline";
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import Stack from "@material-ui/core/Stack";
import { useStyles } from "../style.js";

import { MuiThemeProvider, createTheme } from "@material-ui/core/styles";

 const theme = createTheme({
    palette: {
      primary: {
        main: "#121212",
      },
    },
  });
export default function Login(props) {
  const classes = useStyles(props);

  return(
      <Grid container className={classes.root} 
       item xs={12} md={5}>
            <CssBaseline />
            <MuiThemeProvider theme={theme}>
            <Grid>
                <Typography component="h1" variant="h5">
                   Sign in
                </Typography>
            <form className={classes.form} noValidate>
                  <TextField
                   label="Username"
                   variant="outlined"
                   color="primary"
                   margin="normal"
                   required
                   fullWidth
                   id="username"
                   name="username"
                  />
                 <TextField
                   label="Password"
                   type={"password"}
                   variant="outlined"
                   margin="normal"
                   required
                   fullWidth
                   id="password"
                   name="password"
                  />
                 <Button
                   type="submit"
                   fullWidth
                   color="primary"
                   variant="contained"
                   className={classes.submit}
                  >
                    Sign In
                </Button>
            </form>
          </Grid>
        </MuiThemeProvider>
    </Grid>
  )
}
Enter fullscreen mode Exit fullscreen mode

I have separated the styling codes from this component. The style.js file looks like the following:

import { makeStyles } from "@material-ui/core/styles";


const useStyles = makeStyles((theme) => ({
  root: {
    height: "100vh",
    display: "flex",
    flexDirection: "column",
    margin: theme.spacing(12, 20),
    alignItems: "center",
  },
  form: {
    width: "100%",
    marginTop: theme.spacing(1),
  },
  submit: {
    margin: theme.spacing(3, 0, 2),
    backgroundColor: "#121212",
    color: "#fff"
  },

}));
export { useStyles };

Enter fullscreen mode Exit fullscreen mode

Now, call it from App.js like the following:

...
function App() {
  return (
    <div className="App">
     <Login/>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

You can use any styling libraries to achieve this. If you follow through this, the output should look like this:

login

Step-2: Write a Handle function and Store Access and Refresh Tokens

In this step, we will get the data from the user and do something with it. We need to save this data with React states at first, and then we have to send in the POST API.

Declare a state which will hold the username and the password:

const [data, setData] = useState({
    username: "",
    password: "",
  });

Enter fullscreen mode Exit fullscreen mode

Inside the text fields, put the states in the value property:


         <TextField
              ...
              id="username"
              name="username"
              value={data.username}
              onChange={handleChange}
            />

           <TextField

              id="password"
              name="password"
              value={data.password}
              onChange={handleChange}
            />

Enter fullscreen mode Exit fullscreen mode

We need to track the change of the values with a handler function named handleChange . Write the function like the following:

const handleChange = (e) => {
    const value = e.target.value;
    setData({
      ...data,
      [e.target.name]: value,
    });
  };
Enter fullscreen mode Exit fullscreen mode

Instead of individually doing a setState operation, we at first are checking the value of the target, that is a text input. After that we are spreading the initial state object.

We will do all the API operation with Axios. Install it:

yarn add axios
Enter fullscreen mode Exit fullscreen mode

Put your private login API in a global variable such as login:

const login = "your login api"

Enter fullscreen mode Exit fullscreen mode

Remember how all the text boxes and Sign In button were wrapped in a form tag? We need to pass a handler function to handle the submit operation. Besides, in the API body of type POST, you are required to send the username and password. In our case we can send the entire state object in the API body. The whole handleSubmit will be like the following:

const handleSubmit = (e) => {
    e.preventDefault();
    const userData = {
      username: data.username,
      password: data.password,
    };
    axios.post(login, userData)
      .then((response) => {
        if (response.status === 200) {

          console.log(response.status);
          console.log(response.data);
        }
      })
      .catch((error) => {
        if (error.response) {
          console.log(error.response);
          console.log("server responded");

        } else if (error.request) {
          console.log("network error");
        } else {
          console.log(error);
        }
      });
  };
Enter fullscreen mode Exit fullscreen mode

If you check your login API in Postman, you will get to see the access tokens and refresh tokens in the output.

tokens

We have to save this in the localStorage. Do it inside the following block:

if (response.status === 200) {
           localStorage.setItem("accessToken", response.data["access"]);
          localStorage.setItem("refreshToken", response.data["refresh"]);

         window.location.href = "/home";
        }
Enter fullscreen mode Exit fullscreen mode

Write a bare minimum Home page component to see the change:

import React, { useState } from "react";

export default function Home(){
    return(
        <div>this is home page</div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Oh no! It's in the login page even after our login. That's because, we didn't make any management for routing. Install React Router first:

yarn add react-router-dom
Enter fullscreen mode Exit fullscreen mode

And now add the following code in your App.js:

import { BrowserRouter, Route, Routes } from "react-router-dom";

import Home from "./components/Home";
function App() {
  return (
    <div className="App">
    <BrowserRouter>
            <Routes>
              <Route path="/" element={<Login />} />
              <Route path="/home" element={<Home />} />
            </Routes>
          </BrowserRouter>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step-3: Check the Header of the API Request

Inside your home component, console the tokens:

const access = localStorage.getItem("accessToken");
console.log("access", access)

const refresh = localStorage.getItem("refreshToken");
console.log("refresh", refresh)

Enter fullscreen mode Exit fullscreen mode

token home

Let's make an API request in the home page. Place your private API in a variable:

const details = "any of your private API"

Enter fullscreen mode Exit fullscreen mode

If you do not send a header to this API, you will receive an error code 403 (or whatever the back-end sends). You won't be able to make any API requests without a header. In order to send a header, create a object header in the following format:

const header = {
  "Content-Type": "application/json",
  Authorization: `Bearer ${access}`,
};

Enter fullscreen mode Exit fullscreen mode

You can check in the postman like the following way:

Bearer

Say, your API is a get request. You can write it like the following, with the header you created:

const [data, setData] = useState([]);
  useEffect(() => {
    axios
      .get(details, {
        headers: headers,
      })
      .then((response) => {
        setData(response.data);
      })
      .catch((error) => {
        console.log(error);
      });
  }, [data]);

return <div>this is home page {data}</div>;
Enter fullscreen mode Exit fullscreen mode

Step-4: Conditionally check status codes and hit Refresh API and Logout API if Needed

In this section, we will explore the "Access token is valid?-No" logic branch. In a nutshell, if you find both the tokens invalid, send a request to your logout API. If just the refresh token is invalid, send a request to the Refresh Token API and update the refresh token.

Here's the code:

const logout_url = "your logout url"
const refresh_url = "your refresh url"

// inside the catch block of axios
.catch((error) => {
        if (error.response.status === 401) {
          console.log(error.response);

          axios.post(refresh_url, refresh).then((request) => {
            if (request.status === 200) {
              console.log("refresh valid");
              localStorage.setItem("accessToken", request.data["access"]);
            } else if (request.status === 401) {
              console.log("invalid so logout");
              axios.post(logout_url, headers).then((response) => {
                localStorage.setItem("accessToken", "");
                localStorage.setItem("refreshToken", "");
                console.log("access", response.data["access"]);
                console.log("refresh", response.data["refresh"]);

                window.location.href = "/login";
              });
            }
Enter fullscreen mode Exit fullscreen mode

Step-5: Send Response

If you have followed all these steps, you have implemented an App which is robust with a JWT authentication. Find the final code in my github - https://github.com/Afroza2/React-Apps/tree/jwt .

I think I have given a layout for implementing a JWT authentication in your React app. In some cases, only the access token is provided by the back-end, so you can omit the whole check of the refresh token. Nevertheless, once you implement it, you don't have to think about security again.

💖 💪 🙅 🚩
afrozansenjuti
Afroza Nowshin

Posted on August 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related