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.
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:
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=abracadabraJWT_EXPIRES_IN=1 dayHYGRAPH_URL=VALUEHYGRAPH_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 APIrouter.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 APIrouter.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.passwordres.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:
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;
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;
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;
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.