How to Add Authentication to React Applications

In this article, we’ll explore with the help of a demo, how to set up authentication in a Create React App.

aagem-vadecha-graphcms
Aagam Vadecha
React Authentication

Authentication and Authorization are crucial for any software to be built today. We will implement a basic straightforward JWT-based bearer token authentication.

The components of our setup will be like:

  • Content platform - Hygraph
  • Backend Server - Node.js.
  • Frontend App - Create React App.

It is completely fine to choose your own backend server and database as well. The code for the entire application can be found here.

Setting Up The BackendAnchor

To set up the base, we should begin by creating our User schema in Hygraph. We will keep fields firstname, lastname, email, and password in the schema, it will look something like this:

UserSchema

Once the CMS is configured, we can move on to build the backend Node.js Express API.

Do a yarn init in a fresh folder and install the following dependencies

  • bcryptjs
  • cors
  • dotenv
  • express
  • graphql
  • graphql-request
  • jsonwebtoken

We will be using bcryptjs to hash passwords, express and cors to manage the backend API, dotenv to support environment variables via a .env file, jsonwebtoken to sign, and decode the jwt-tokens for authentication, and finally graphql, graphql-request to interact with Hygraph.

Let's begin with some base backend server setup.

Please add the following files .env

JWT_SECRET=abracadabra
JWT_EXPIRES_IN=1 day
HYGRAPH_URL=VALUE
HYGRAPH_PERMANENTAUTH_TOKEN=VALUE

index.js app.js graphql/client.js graphql/mutations.js

We can now move on to building the controller where the main backend logic resides.

Here is the SignUp API

//SIGN UP API
router.post('/auth/signup', async (req, res) => {
try {
const {
email, password, firstname, lastname,
} = req.body;
if (!email || !password || !firstname || !lastname) {
res.status(400).end();
}
const hashedPassword = await bcrypt.hash(password, 8);
const userData = {
email,
password: hashedPassword,
firstname,
lastname,
};
const response = await gqlClient.request(CreateNextUserMutation, { userData });
if (!response?.createNextUser) {
console.log('CreateUser Failed, Response: ', response);
res.status(400).end()
}
const token = jwt.sign({ user: response.createNextUser }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
res.send({ user: response.createNextUser, token });
} catch (err) {
console.log('POST auth/signup, Something Went Wrong: ', err);
res.status(400).send({ error: true, message: err.message });
}
});

For Signup, we get the user details from frontend, validate it, then we hash the password and save the user in database, finally we sign a jwt-token with the user details and send the jwt-token back to client.

Here is the SignIn code

router.post('/auth/signin', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
res.status(400).end();
return;
}
const getUserResponse = await gqlClient.request(GetUserByEmailQuery, { email });
const { nextUser } = getUserResponse;
if (!nextUser) {
res.status(400).json({ msg: 'Invalid Email Or Password' });
return;
}
const { password: hashedPassword } = nextUser;
const isMatch = await bcrypt.compare(password, hashedPassword);
if (!isMatch) {
res.status(400).json({ msg: 'Invalid Email Or Password' });
return;
}
const token = jwt.sign({
id: nextUser.id,
email: nextUser.email
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
res.status(200).json({ token });
} catch (err) {
console.log('POST auth/signin, Something Went Wrong: ', err);
res.status(400).send({ error: true, message: err.message });
}
})

We simply validate the email and password and send an appropriate response back to the client.

Also, one final API to Get user from a provided jwt token.

// GET USER FROM TOKEN API
router.get('/auth/me', async (req, res) => {
const defaultReturnObject = { authenticated: false, user: null };
try {
const token = String(req?.headers?.authorization?.replace('Bearer ', ''));
const decoded = jwt.verify(token, JWT_SECRET);
const getUserResponse = await gqlClient.request(GetUserByEmailQuery, { email: decoded.email });
const { nextUser } = getUserResponse;
if (!nextUser) {
res.status(400).json(defaultReturnObject);
return;
}
delete nextUser.password
res.status(200).json({ authenticated: true, user: nextUser });
}
catch (err) {
console.log('POST auth/me, Something Went Wrong', err);
res.status(400).json(defaultReturnObject);
}
})

The entire controller can be found in the controller/user.js file.

Now go to the root of this setup and node index.js, you should be able to see that the backend app is now running, and with this our Authentication backend setup is complete!

Setting Up The FrontendAnchor

To create a React application move to a fresh folder and type npx create-react-app frontend To this app, add axios and react-router-dom dependencies. I’ll be using tailwind css for this project feel free to use your own css framework. To begin with a clean slate, clean up the create-react-app boilerplate css and test files.

The strategy here to handle authentication is that when we hit the backend SignIn API, it will give us a jwt token, we will save that token somewhere on the client side like the local storage. Then on every route visit in the frontend we will just check if the token is present in the browser and if we get the user by calling the Get Me API , we will write this logic inside a custom react hook for resuability.

Note: To reduce the scope of this demo, we have used local storage. It is not ideal to store tokens in local storage, cookies / in-memory would be a safer option.

First, add this self-explanatory code:

  • src/utils/constants.js - here
  • src/lib/common.js - here

Now we will add a custom hook to check for the Authenticated user. This hook can be used inside any react component to get the Current Authenticated User.

src/lib/customHooks.js

import { useState, useEffect } from 'react';
import { getAuthenticatedUser } from './common';
import { APP_ROUTES } from '../utils/constants';
import { useNavigate } from 'react-router-dom';
export function useUser() {
const [user, setUser] = useState(null);
const [authenticated, setAuthenticated] = useState(false);
const navigate = useNavigate();
useEffect(() => {
async function getUserDetails() {
const { authenticated, user } = await getAuthenticatedUser();
if (!authenticated) {
navigate(APP_ROUTES.SIGN_IN);
return;
}
setUser(user);
setAuthenticated(authenticated);
}
getUserDetails();
}, []);
return { user, authenticated };
}

Now we can the main container with routes and three simple components for our app.

src/App.js

import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
import Dashboard from './components/Dashboard';
import SignIn from './components/SignIn';
import SignUp from './components/SignUp';
import { APP_ROUTES } from './utils/constants';
function App() {
return (
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Navigate to={APP_ROUTES.DASHBOARD} />} />
<Route path={APP_ROUTES.SIGN_UP} exact element={<SignUp />} />
<Route path={APP_ROUTES.SIGN_IN} element={<SignIn />} />
<Route path={APP_ROUTES.DASHBOARD} element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
export default App;

components/SignUp.jsx

import React from 'react';
import axios from 'axios';
import { useState } from 'react';
import { API_ROUTES, APP_ROUTES } from '../utils/constants';
import { Link, useNavigate } from 'react-router-dom';
const SignUp = () => {
const navigate = useNavigate()
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [firstname, setFirstname] = useState('');
const [lastname, setLastname] = useState('');
const [isLoading, setIsLoading] = useState(false);
const signUp = async () => {
try {
setIsLoading(true);
const response = await axios({
method: 'POST',
url: API_ROUTES.SIGN_UP,
data: {
email,
password,
firstname,
lastname
}
});
if (!response?.data?.token) {
console.log('Something went wrong during signing up: ', response);
return;
}
navigate(APP_ROUTES.SIGN_IN);
}
catch (err) {
console.log('Some error occured during signing up: ', err);
}
finally {
setIsLoading(false);
}
};
return (
// SIGN UP FORM TEMPLATE
);
}
export default SignUp;

SignUp

components/SignIn.jsx

import React from 'react';
import axios from 'axios';
import { useState } from 'react';
import { API_ROUTES, APP_ROUTES } from '../utils/constants';
import { Link, useNavigate } from 'react-router-dom';
import { useUser } from '../lib/customHooks';
import { storeTokenInLocalStorage } from '../lib/common';
const SignIn = () => {
const navigate = useNavigate();
const { user, authenticated } = useUser();
if (user || authenticated) {
navigate(APP_ROUTES.DASHBOARD)
}
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const signIn = async () => {
try {
setIsLoading(true);
const response = await axios({
method: 'post',
url: API_ROUTES.SIGN_IN,
data: {
email,
password
}
});
if (!response?.data?.token) {
console.log('Something went wrong during signing in: ', response);
return;
}
storeTokenInLocalStorage(response.data.token);
navigate(APP_ROUTES.DASHBOARD)
}
catch (err) {
console.log('Some error occured during signing in: ', err);
}
finally {
setIsLoading(false);
}
};
return (
// SIGN IN FORM TEMPLATE
);
}
export default SignIn;

SignIn

That’s it! Try signing up a user with the signup page, it will call our backend signup API and register the user in the database. After that, you can sign in using the above component, once signed in, we store the token in local storage and then we redirect the user to some page that requires a user to be authenticated.

So finally, lets build the Dashboard component which will be a protected route, we will make use of the useUser() custom hook that we made earlier to get the authenticated user.

components/Dashboard.jsx

import React from 'react';
import { useUser } from '../lib/customHooks';
const Dashboard = () => {
const { user, authenticated } = useUser();
if (!user || !authenticated) {
return <div className="p-16 bg-gray-800 h-screen">
<div className="text-2xl mb-4 font-bold text-white">Dashboard</div>
<div className="ml-2 w-8 h-8 border-l-2 rounded-full animate-spin border-white" />
</div>;
}
return (
<div className="p-16 bg-gray-800 h-screen">
<div className="text-2xl mb-4 font-bold text-white"> Dashboard </div>
{
user &&
<div className='text-white'>
<div className="text-lg text-bold mb-2"> User Details </div>
<div className="flex">
<div className="w-24 font-medium">
<div> Email : </div>
<div> Firstname : </div>
<div> Lastname : </div>
</div>
<div>
<div> {user.email} </div>
<div> {user.firstname} </div>
<div> {user.lastname} </div>
</div>
</div>
</div>
}
</div>
);
}
export default Dashboard;

Dashboard

If you delete the token from local storage and try to go to route /dashboard you’ll be redirected to /signin, the useUser() hook handles that redirection.

Well done, a complete application right from the database all the way to a React Frontend with Authentication is ready! If you’re looking out to build a robust application that scales well in production, you might want to take a look at how a create react app differs from Next.js, the production framework for React!

This was a very basic authentication setup that we did and something that every developer should have an idea about. This can be expanded further to support a number of use cases but is not much scalable after a certain point as real-world scenarios can be much more complex. Managing multiple logins, revoking access, SSO Logins, OAuth flows, fine-grained authorization rules, and managing fingerprints, all these advanced features are required by many software products. In reality, it is quite difficult to build and manage Authentication and Authorization from scratch. OAuth flows can get tricky, you have to keep all your Auth API up-to-date with the latest standards and look out for all vulnerabilities in your system. If you build your own Auth, you have to maintain it responsibly as security issues are not something you can stall. That is why many teams who want to focus more on building their actual product and not be tangled with Authentication and Authorization workflows opt for services like AWS Cognito or Auth0.