Putting security measures in place on your React apps to ensure that users register with strong passwords is a top priority for every developer. As users, sometimes we easily forget the passwords we use on websites. Normally, users who create different accounts with unique passwords are more likely to forget their passwords.
If you’ve used Google OAuth to register for accounts on websites before, you may have received an email from Google telling you that “Someone has forcefully logged into your account, please change your password“.
So, it’s crucial to add a forgot/reset password feature to your React app that will allow users to securely reset their passwords when they forget them or when their passwords are found in data breaches.
In this article, you will learn how to implement forgot/reset password feature via email in React using Axios.
Articles in this series:
- React.js and Axios: User Registration and Email Verification
- Forgot/Reset Password in React.js and Axios
Prerequisites
To follow along with this tutorial, here are some requirements:
- You should have a basic understanding of JavaScript, TypeScript, and React.js
- You should have Node.js installed on your machine.
- Basic knowledge of how JSON Web Token authentication works will be beneficial.
Forgot/Reset Password Workflow in React
The workflow for resetting passwords can be done in different ways depending on the usability and security of your application. This article will implement a standard forgot/reset password workflow. The illustration below will demonstrate how the workflow will work.
The user forgets the password
When a user tries to log in and the server returns an error indicating that either the email or password is invalid, the user can click on the Forgot Password link on the login page.
Also, in the event that the user’s account was compromised, the user can click on the Forgot Password link to start the password reset process.
Request the password reset link
On the forgot password page, the user will provide the email and click on the send reset code button. Next, React will make an Axios POST request with the email address included in the payload to the /api/auth/forgotpassword
endpoint on the server.
The server will then check the database to see if a user with that email exists, generate the password reset HTML template and send the password reset link to the user’s email inbox.
From the password reset email, the user can click on the “Reset Password” button to be redirected to the password reset page.
Reset the password
On the password reset page, the user will enter the new password and make an Axios PATCH request to the /api/auth/resetpassword/:resetCode
endpoint on the server.
On a successful password update, an alert notification will be displayed to the user, and React will redirect the user to the login page.
Run the React Forgot/Reset Password Example Locally
- Download or clone the project source code from https://github.com/wpcodevo/reactjs-axios.
- Open the project with your IDE and change the Git branch to
forgot-reset-password
. - Run
yarn
oryarn install
from the terminal in the root directory to install all the dependencies. - Start the Vite development server by running
yarn dev
. - Open a new tab and go to the application at
localhost:3000
.
Step 1 – Run the Backend Server
Below are two backend APIs built with Node.js and Golang:
Node.js Forgot/Reset Password Server
For full details about how to create the forgot/reset password functionality with Node.js and Prisma, see the post API with Node.js, Prisma & PostgreSQL: Forget/Reset Password. However, to get the Node.js server up and running quickly so that you can follow along with this tutorial, follow the steps below.
- Install Node.js from https://nodejs.org/. Install Yarn with
npm install --global yarn
. - Download or clone the project source code from https://github.com/wpcodevo/node_prisma_postgresql.
- Open the project with your IDE or text editor and change the Git branch to
forgot_reset_password
. - Run
yarn install
oryarn
to install all the required dependencies. - Duplicate the
example.env
file and rename the duplicated one to.env
- Start the Redis and PostgreSQL Docker containers by running
docker-compose up -d
- Create the Prisma migration file and push the schema to the PostgreSQL database by running
yarn db:migrate && yarn db:push
. - Open the
.env
file and add your SMTP credentials or open thesrc/app.ts
file, uncomment the Nodemailer code, and start the Node.js server withyarn start
to generate the Nodemailer SMTP test credentials. - Copy the generated SMTP credentials and add them to the
.env
file. After that comment out the Nodemailer code again and save the file to restart the server.
Golang Forgot/Reset Password Server
For full details about how to create the Forgot/Reset Password with Golang and MongoDB, see the post Forgot/Reset Passwords in Golang with SMTP HTML Email.
Alternatively, you can follow these steps to get the Golang server up and running quickly.
- Install Golang from https://go.dev/doc/install
- Download or clone the project source code from https://github.com/wpcodevo/golang-mongodb-api.
- Change the Git branch of the project to
golang-mongodb-reset-password
from the terminal in the project root folder. - Run
go mod tidy
to install all the packages. - Start the Redis and MongoDB Docker containers with
docker-compose up -d
. - Copy and paste the
example.env
file and rename the copied one toapp.env
- Add your SMTP credentials to the
app.env
file or register for an account on mailtrap.io and add the SMTP credentials to theapp.env
file - Start the Golang Gin server with
go run main.go
.
Step 2 – Create the Forgot Password Page
The forgot password page contains a form built with the React Hook Form library that contains an email field for requesting the password reset link.
The form validation rules are defined with Zod, a popular schema validation library that can be used in both the browser and Node.js. You can see https://github.com/colinhacks/zod for more details.
src/pages/forgotpassword.page.tsx
import { object, string, TypeOf } from "zod";
import { useEffect } from "react";
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import useStore from "../store";
import { authApi } from "../api/authApi";
import { GenericResponse } from "../api/types";
const forgotPasswordchema = object({
email: string().min(1, "Email is required").email("Invalid email address"),
});
export type ForgotPasswordInput = TypeOf<typeof forgotPasswordchema>;
const ForgotPasswordPage = () => {
const store = useStore();
const methods = useForm<ForgotPasswordInput>({
resolver: zodResolver(forgotPasswordchema),
});
const {
reset,
handleSubmit,
formState: { isSubmitSuccessful },
} = methods;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitSuccessful]);
const forgotPassword = async (data: ForgotPasswordInput) => {
try {
store.setRequestLoading(true);
const response = await authApi.post<GenericResponse>(
`auth/forgotpassword`,
data
);
store.setRequestLoading(false);
toast.success(response.data.message as string, {
position: "top-right",
});
} catch (error: any) {
store.setRequestLoading(false);
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
toast.error(resMessage, {
position: "top-right",
});
}
};
const onSubmitHandler: SubmitHandler<ForgotPasswordInput> = (values) => {
forgotPassword(values);
};
return (
<section className="bg-ct-blue-600 min-h-screen grid place-items-center">
<div className="w-full">
<h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-7">
Forgot Password
</h1>
<FormProvider {...methods}>
<form
onSubmit={handleSubmit(onSubmitHandler)}
className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
>
<FormInput label="Email Address" name="email" type="email" />
<LoadingButton
loading={store.requestLoading}
textColor="text-ct-blue-600"
>
Send Reset Code
</LoadingButton>
</form>
</FormProvider>
</div>
</section>
);
};
export default ForgotPasswordPage;
The onSubmitHandler
method will post the user’s email to the API server by evoking the forgotPassword
function. On successful submission, the API server will generate the password reset link and send it to the provided email address.
When the request fails, an alert notification will be displayed using the React–Toastify library.
Step 3 – Create the Password Reset Page
The password reset page contains password and password confirmation inputs built with the React Hook Form library and tailwind CSS.
Like the forgot password component, the form validation rules are defined with the Zod schema validation library which React Hook Form supports via the @hookform/resolvers
package.
src/pages/resetpassword.page.tsx
import { object, string, TypeOf } from "zod";
import { useEffect } from "react";
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import { Link, useNavigate, useParams } from "react-router-dom";
import useStore from "../store";
import { authApi } from "../api/authApi";
import { GenericResponse } from "../api/types";
const resetPasswordSchema = object({
password: string().min(1, "Current password is required"),
passwordConfirm: string().min(1, "Please confirm your password"),
});
export type ResetPasswordInput = TypeOf<typeof resetPasswordSchema>;
const ResetPasswordPage = () => {
const store = useStore();
const navigate = useNavigate();
const { resetCode } = useParams();
const methods = useForm<ResetPasswordInput>({
resolver: zodResolver(resetPasswordSchema),
});
const {
reset,
handleSubmit,
formState: { isSubmitSuccessful },
} = methods;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitSuccessful]);
const resetPassword = async (data: ResetPasswordInput) => {
try {
store.setRequestLoading(true);
const response = await authApi.patch<GenericResponse>(
`auth/resetpassword/${resetCode}`,
data
);
store.setRequestLoading(false);
toast.success(response.data.message as string, {
position: "top-right",
});
navigate("/login");
} catch (error: any) {
store.setRequestLoading(false);
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
toast.error(resMessage, {
position: "top-right",
});
}
};
const onSubmitHandler: SubmitHandler<ResetPasswordInput> = (values) => {
if (resetCode) {
resetPassword(values);
} else {
toast.error("Please provide the password reset code", {
position: "top-right",
});
}
};
return (
<section className="bg-ct-blue-600 min-h-screen grid place-items-center">
<div className="w-full">
<h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-7">
Reset Password
</h1>
<FormProvider {...methods}>
<form
onSubmit={handleSubmit(onSubmitHandler)}
className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
>
<FormInput label="New Password" name="password" type="password" />
<FormInput
label="Confirm Password"
name="passwordConfirm"
type="password"
/>
<LoadingButton
loading={store.requestLoading}
textColor="text-ct-blue-600"
>
Reset Password
</LoadingButton>
</form>
</FormProvider>
</div>
</section>
);
};
export default ResetPasswordPage;
When the form is submitted, the resetPassword
method will be evoked to post the new password to the API server. On a successful password update, an alert notification will be displayed, and React will redirect the user to the login page.
Step 4 – Login with the new Password
The login page contains a form built with the React Hook Form library that contains email and password inputs for signing users into the app.
src/pages/login.page.tsx
import { object, string, TypeOf } from "zod";
import { useEffect } from "react";
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import { Link, useNavigate } from "react-router-dom";
import useStore from "../store";
import { ILoginResponse } from "../api/types";
import { authApi } from "../api/authApi";
const loginSchema = object({
email: string()
.min(1, "Email address is required")
.email("Email Address is invalid"),
password: string()
.min(1, "Password is required")
.min(8, "Password must be more than 8 characters")
.max(32, "Password must be less than 32 characters"),
});
export type LoginInput = TypeOf<typeof loginSchema>;
const LoginPage = () => {
const store = useStore();
const navigate = useNavigate();
const methods = useForm<LoginInput>({
resolver: zodResolver(loginSchema),
});
const {
reset,
handleSubmit,
formState: { isSubmitSuccessful },
} = methods;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitSuccessful]);
const loginUser = async (data: LoginInput) => {
try {
store.setRequestLoading(true);
await authApi.post<ILoginResponse>("/auth/login", data);
store.setRequestLoading(false);
navigate("/profile");
} catch (error: any) {
store.setRequestLoading(false);
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
toast.error(resMessage, {
position: "top-right",
});
}
};
const onSubmitHandler: SubmitHandler<LoginInput> = (values) => {
loginUser(values);
};
return (
<section className="bg-ct-blue-600 min-h-screen grid place-items-center">
<div className="w-full">
<h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
Welcome Back
</h1>
<h2 className="text-lg text-center mb-4 text-ct-dark-200">
Login to have access
</h2>
<FormProvider {...methods}>
<form
onSubmit={handleSubmit(onSubmitHandler)}
className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
>
<FormInput label="Email" name="email" type="email" />
<FormInput label="Password" name="password" type="password" />
<div className="text-right">
<Link to="/forgotpassword" className="">
Forgot Password?
</Link>
</div>
<LoadingButton
loading={store.requestLoading}
textColor="text-ct-blue-600"
>
Login
</LoadingButton>
<span className="block">
Need an account?{" "}
<Link to="/register" className="text-ct-blue-600">
Sign Up Here
</Link>
</span>
</form>
</FormProvider>
</div>
</section>
);
};
export default LoginPage;
We created the input field in a different file so we used the FormProvider
component provided by React Hook Form to make the form context available to the input components.
Conclusion
Congratulation on reaching the end. In this article, you learned how to implement forgot/reset password functionality via email in a React.js app.
You can find the complete source code of this project in my GitHub repository.
You have forgoteen how react will know which password need to be changed.
there is some token exchange implemenation is missing in your blog please also add that
Thank you for pointing that out. This article is a continuation of a previous one where we implemented authentication using JWTs.
I could have included the JWT authentication alongside the forgot/reset password functionality in the same article, but it would have made it lengthy and not everyone would prefer scrolling through extensive content.