Authentication systems play a crucial role in providing a seamless and secure user experience. An authentication workflow typically involves two processes: sign-up and log-in.

As the number of online services increases, people create accounts, and each account requires unique login credentials. However, this makes it easy to forget or confuse login credentials. To address this, your app should implement a password reset feature that lets a user reset their password conveniently and securely.

Set Up the React Project

You can implement a password reset workflow in various ways—there isn't a universal standard that every application should follow. Instead, you should tailor the approach you choose to meet the specific needs of your application.

The workflow you’ll learn about here includes the following steps:

Password-Reset-Workflow

To get started, quickly bootstrap a React project. Next, install Axios, a JavaScript HTTP request library.

        npm install axios
    

You can find the project's code in this GitHub repository.

Create a Login Component

In the src directory, create a new components/Login.js file and add the following code. Start by defining the password reset process:

        import axios from "axios";
import React, { useState } from "react";
import { useContext } from "react";
import { RecoveryContext } from "../App";
import "./global.component.css";

export default function Login() {
  const { setPage, setOTP, setEmail } = useContext(RecoveryContext);
  const [userEmail, setUserEmail] = useState("");

  function sendOtp() {
    if (userEmail) {
      axios.get(`http://localhost:5000/check_email?email=${userEmail}`).then((response) => {
        if (response.status === 200) {
          const OTP = Math.floor(Math.random() * 9000 + 1000);
          console.log(OTP);
          setOTP(OTP);
          setEmail(userEmail);

          axios.post("http://localhost:5000/send_email", {
            OTP,
            recipient_email: userEmail,
          })
          .then(() => setPage("otp"))
          .catch(console.log);
        } else {
          alert("User with this email does not exist!");
          console.log(response.data.message);
        }}).catch(console.log);
    } else {
      alert("Please enter your email");
    }}

This code creates a function that sends a One-Time Password (OTP) to a user's email address. It first verifies the user by checking their email in the database before generating and sending the OTP. Finally, it updates the UI with the OTP page.

Complete the login component by adding code to render the login JSX form element:

          return (
    <div>
      <h2>Login</h2>

      <form>
        <label /> Email:
        <input type="email" value={userEmail} onChange={(e) => { setUserEmail(e.target.value) }} />

        <label /> Password:
        <input type="password" />

        <button type="submit">login</button>
      </form>
      <a href="#" onClick={() => sendOtp()}>
        Forgot Password
      </a>
    </div>
  );
}

Create an OTP Verification Component

To ensure the validity of a code entered by a user, you need to compare it to the code sent to their email.

Create a new components/OTPInput.js file and add this code:

        import React, { useState, useContext, useEffect } from "react";
import { RecoveryContext } from "../App";
import axios from "axios";
import "./global.component.css";

export default function OTPInput() {
  const { email, otp, setPage } = useContext(RecoveryContext);
  const [OTPinput, setOTPinput] = useState( "");

  function verifyOTP() {
    if (parseInt(OTPinput) === otp) {
      setPage("reset");
    } else {
      alert("The code you have entered is not correct, try again re-send the link");
    }
  }

The code creates a React component where users verify their OTP code. It checks that the entered code matches the one stored in the context object. If it's valid, it displays the password-reset page. Conversely, it shows an alert prompting the user to try again or resend the OTP.

You can check the code in this repository that implements a function for resending OTPs and an expiration timer for the OTP code.

Finally, render the input JSX elements.

          return (
    <div>
      <h3>Email Verification</h3>
      <p>We have sent a verification code to your email.</p>
      <form>
         <input type="text" value={OTPinput} onChange={(e) => { setOTPinput(e.target.value) }} />
          <button onClick={() => verifyOTP()}>Verify Account</button>
          <a onClick={() => resendOTP()} > Didn't receive code?
            {disable ? `Resend OTP in ${timerCount}s` : " Resend OTP"}
          </a>
      </form>
    </div>
  );}

Create the Reset Password Component

Create a new components/Reset.js file and add this code:

        import React, {useState, useContext} from "react";
import { RecoveryContext } from "../App";
import axios from "axios";
import "./global.component.css";

export default function Reset() {
  const [password, setPassword] = useState("");
  const { setPage, email } = useContext(RecoveryContext);

  function changePassword() {
    if (password) {
      try {
        axios.put("http://localhost:5000/update-password", {
          email:email,
          newPassword: password,
        }).then(() => setPage("login"));

        return alert("Password changed successfully, please login!");
      } catch (error) {console.log(error);}}
    return alert("Please enter your new Password");
 }

  return (
    <div>
      <h2> Change Password </h2>
        <form>
          <label /> New Password:
          <input
            type="password"
            placeholder="........"
            required=""
            value={password}
            onChange={(e) => setPassword(e.target.value)} />
          <button onClick={() => changePassword()}>Reset passwod </button>
        </form>
    </div>
  );
}

This code renders a form that allows users to enter a new password. When the user clicks on submit, it will send a request to the server to update their password in the database. It will then update the UI if the request is successful.

Update Your App.js Component

Make the changes below to your src/App.js file:

        import { useState, createContext } from "react";
import Login from "./components/Login";
import OTPInput from "./components/OTPInput";
import Reset from "./components/Reset";
import "./App.css";
export const RecoveryContext = createContext();

export default function App() {
  const [page, setPage] = useState("login");
  const [email, setEmail] = useState("");
  const [otp, setOTP] = useState("");

  function NavigateComponents() {
    if (page === "login") return <Login />;
    if (page === "otp") return <OTPInput />;
    if (page === "reset") return <Reset />;
  }

  return (
    <div className="App-header">
      <RecoveryContext.Provider
        value={{ page, setPage, otp, setOTP, email, setEmail }}>
        <div>
          <NavigateComponents />
        </div>
      </RecoveryContext.Provider>
    </div>
  );
}

This code defines a context object that manages the app's state, which includes the user's email, the OTP code, and the various pages within the app. Essentially, the context object makes it possible to pass the required states between different components—an alternative to using props.

It also includes a function that handles page navigation with ease without needing to re-render whole components.

Set Up an Express.js Server

With the client setup, configure a backend authentication service to handle the password reset functionality.

To get started, create an Express web server, and install these packages:

        npm install cors dotenv nodemailer mongoose
    

Next, create a MongoDB database or configure a MongoDB cluster on the cloud. Then copy the connection string provided, create an ENV file in the root directory, and paste the connection string.

To finish, you need to configure the database connection and define the data models for your user data. Use the code in this repository to set up the database connection and define the data models.

Define the API Routes

A backend service ideally has several routes that handle clients' HTTP requests. In this case, you'll need to define three routes that'll manage the send-email, email verification, and update-password API requests from the React client.

Create a new file called userRoutes.js in the root directory and add the following code:

        const express = require('express');
const router = express.Router();
const userControllers = require('../controllers/userControllers');

router.get('/check_email', userControllers.checkEmail);
router.put('/update-password', userControllers.updatePassword);
router.post('/send_email', userControllers.sendEmail);

module.exports = router;

Controllers for the API Routes

Controllers are responsible for processing clients' HTTP requests. Once, a client makes a request to a particular API route, a controller function gets invoked and executed to process the request and return an appropriate response.

Create a new controllers/userControllers.js file and add the code below.

Use the code in this repository to define controllers for the email verification and update-password API routes.

Start by defining the send email controller:

        exports.sendEmail = (req, res) => {
  const transporter = nodemailer.createTransport({
    service: 'gmail',
    secure: true,
    auth: {
      user: process.env.MY_EMAIL,
      pass: process.env.APP_PASSWORD,
    },
  });

  const { recipient_email, OTP } = req.body;

  const mailOptions = {
    from: process.env.MY_EMAIL,
    to: recipient_email,
    subject: 'PASSWORD RESET',
    html: `<html>
             <body>
               <h2>Password Recovery</h2>
               <p>Use this OTP to reset your password. OTP is valid for 1 minute</p>
               <h3>${OTP}</h3>
             </body>
           </html>`,
  };

  transporter.sendMail(mailOptions, (error, info) => {
    if (error) {
      console.log(error);
      res.status(500).send({ message: "An error occurred while sending the email" });
    } else {
      console.log('Email sent: ' + info.response);
      res.status(200).send({ message: "Email sent successfully" });
    }
  });
};

This code defines a function that uses Nodemailer to send an email with a OTP reset to a specified recipient. It sets up a transporter using your own Gmail account and password.

To get your Gmail app password, you need to generate an app password in your Google account settings. You’ll then use this password in place of your regular Gmail password for authenticating the Nodemailer.

Configure the Server Entry Point

Create a server.js file in the root directory and add this code:

        const express = require('express');
const cors = require('cors');
const app = express();
const port = 5000;
require('dotenv').config();
const nodemailer = require('nodemailer');
const connectDB = require('./utils/dbconfig');
connectDB();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());
const userRoutes = require('./routes/userRoutes');
app.use('/', userRoutes);

app.listen(port, () => {
  console.log(`Server is listening at http://localhost:${port}`);
});

With both the client and server set up, you can run the development servers to test the password functionality.

Building a Custom Password-Reset Service

Creating a password reset system by tailoring it to your application and its users is the best approach, even though paid, pre-built solutions exist. While designing this feature, you should take into account both user experience and security, as attacks are a constant threat.