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 :
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:
- Create a React App, then create a Login component with user input and a submit button and call it from App.js.
- Write a handleSubmit where you will save the user input as states and also the access and refresh tokens in the localStorage.
- 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.
- Conditionally check status codes and hit Refresh API and Logout API if Needed
- 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/
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
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>
)
}
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 };
Now, call it from App.js like the following:
function App() {
return (
<div className="App">
<Login/>
</div>
);
}
You can use any styling libraries to achieve this. If you follow through this, the output should look like this:
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: "",
});
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}
/>
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,
});
};
Instead of individually doing a setState operation, we at first are checking the value of the target, that is text input. After that, we are spreading the initial state object.
We will do all the API operations with Axios. Install it:
yarn add axios
Put your private login API in a global variable such as login:
const login = "your login api"
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);
}
});
};
If you check your login API in Postman, you will get to see the access tokens and refresh tokens in the output.
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";
}
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>
)
}
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
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>
);
}
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)
Let's make an API request in the home page. Place your private API in a variable:
const details = "any of your private API"
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}`,
};
You can check in the postman like the following way:
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>;
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";
});
}
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.