First, install jsonwebtoken
, @sendgrid/mail
, and bcrypt
as dependencies, and @types/jsonwebtoken
and @types/bcrypt
as dev dependencies in your Express.js project. We will use these packages to create secure tokens, send emails with password reset links, and create password hashes respectively.
Create forgot password route
With the following code, let's create a forgot-password
route that takes the user's email, checks if the user exists in the database, and sends him/her a password reset link.
import mail from '@sendgrid/mail';
import jwt from 'jsonwebtoken';
import User from '../models/user';
import { passwordResetTemplate } from '../lib/emailTemplates';
router.post('/forgot-password', async (req, res) => {
const { email } = req.body;
if (!email) {
console.log('Email is required');
res.status(400);
throw new Error('Email is required');
}
try {
const user = await User.findOne({ email }).orFail();
const token = jwt.sign(
{ password: user.password },
process.env.JWT_SECRET as string,
{ expiresIn: '15m' }
);
const link = `${process.env.CLIENT_URL}/reset-password/${user._id}/${token}`;
await mail.send(passwordResetTemplate(user.toObject(), link));
res.status(200).json('Password reset details sent to your email');
} catch (err) {
console.log(err);
throw err;
}
});
In the above code, first, we get the user's email
from the req.body
. We check for the email and throw an error if no email is provided. After that, we query the database to find the user with the provided email.
If the user exists, we use the jwt.sign
method to create a token
with the user's password hash. We do this to keep track of the password the user is trying to reset and restrict him/her from resetting the password more than once using the same link. We also set the token expiry so the user must reset the password within 15 minutes of the request.
Next, we construct a link
with the user's _id
and the token
. This is the frontend URL where the user will be navigated to input the new password and perform the reset. Make sure a corresponding page to this link
exists in your frontend application.
Finally, we send an email using the SendGrid mail.send
method with the password reset link.
If you are interested in learning about sending emails using SendGrid and Next.js or Express.js, I have an article on Send emails using SendGrid from Next.js and Express.js applications that provides further insights.
Create reset password route
Now, to handle the creation of a new password, let's create a reset-password
route with the following code:
import bcrypt from 'bcrypt';
import mail from '@sendgrid/mail';
import User from '../models/user';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { passwordResetConfirmationTemplate } from '../lib/emailTemplates';
router.patch('/reset-password/:userId/:token', async (req, res) => {
const { password } = req.body;
if (!password) {
console.log('Password is required');
res.status(400);
throw new Error('Password is required');
}
const { userId, token } = req.params;
try {
const user = await User.findById(userId).orFail();
const decoded = jwt.verify(
token,
process.env.JWT_SECRET as string
) as JwtPayload;
if (decoded.password !== user.password) {
console.log('Invalid token');
res.status(400);
throw new Error('Invalid token');
}
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
await User.findOneAndUpdate(
{ _id: userId },
{
password: hashedPassword,
}
).orFail();
await mail.send(passwordResetConfirmationTemplate(user.toObject()));
res.status(201).json('Password reset successful');
} catch (err) {
console.log(err);
throw err;
}
});
In the above code, first, we get the password
from the req.body
. We check for the password and throw an error if no password is provided. After that, we get the userId
and token
from the req.params
and query the database to find the user with the provided ID.
If the user exists, we decode the token
and create a decoded
variable by using the jwt.verify
method. The decoded
variable is the object that we stored in the token
when signed from the forgot-password
route. Now, we check if the password hash stored in the decoded
object matches with the hash stored in the database. If it doesn't, we throw an error.
Otherwise, we create salt
using the bcrypt.genSalt
method, hash the password
by using the bcrypt.hash
method, update the user in the database with the new hash, and send a confirmation email saying the password reset is successful.
By the way, if you are interested in learning about creating secure authentication using HTTP-only cookies, JSON Web Tokens, and Express.js, I have an article on Create secure authentication using HTTP-only cookies in Express.js that provides further insights.
Behind the scenes
So, for the first request made to the reset-password
route, the password hash stored in the decoded
object will be the same as the hash stored in the database. Consequently, it will pass the check, and the password reset will be successful as long as the request is made within 15 minutes.
However, on a successful password reset, a new hash will be saved to the database. Any subsequent request made to the reset-password
route using the same link, the password hash stored in the decoded
object won't match with the hash stored in the database. This will fail the check and throw an error, preventing the user from resetting the password again.
That's it! You can now use these endpoints in your frontend application to build the forgot and reset password functionalities. Hope you found the article useful. Feel free to share the article with others who might find it useful as well. Cheers!