Implementing Password Reset Functionality in Node.js with JWT and Nodemailer

November 2, 2024 (2w ago)

Implementing Password Reset Functionality in Node.js with JWT and Nodemailer

Password reset functionality is essential for any application that requires user authentication. By combining JWT (JSON Web Token) with Nodemailer, you can build a secure password reset process that allows users to reset their passwords without exposing sensitive data. This guide will walk you through implementing a password reset feature in Node.js with Express, Mongoose, and Nodemailer.


Overview of the Password Reset Process

The password reset process generally involves the following steps:

  1. Password Reset Request: The user submits their email to request a password reset.
  2. Reset Email: The server sends an email with a secure link containing a unique reset token.
  3. Token Verification: The user clicks the link, and the server verifies the token to confirm the user’s identity.
  4. Password Update: The user enters a new password, and the server updates their password if the token is valid.

Using JWT for the reset token allows the process to remain stateless, with an expiration time to ensure security.


Setting Up the Project

We’ll build upon the Node.js, Express, and Mongoose setup with jsonwebtoken for JWT and nodemailer for sending reset emails.

Step 1: Install the Required Dependencies

If you’re starting a new project, initialize it and install the required packages:

mkdir password-reset
cd password-reset
npm init -y
npm install express mongoose jsonwebtoken bcryptjs nodemailer dotenv

Configuring Environment Variables

Create a .env file to store environment variables, such as the MongoDB URI, JWT secret, and email credentials.

MONGODB_URI=mongodb://localhost:27017/password_reset
JWT_SECRET=your_jwt_secret
PORT=5000
EMAIL_USER=your_email@example.com
EMAIL_PASS=your_email_password

For email credentials, you can use a service like Ethereal Email for testing or your own email provider.


Setting Up the User Model

Create a User model with Mongoose, including fields for storing user information.

models/User.js

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
 
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
});
 
// Hash password before saving
userSchema.pre("save", async function (next) {
  if (this.isModified("password")) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});
 
// Compare password for login
userSchema.methods.comparePassword = async function (password) {
  return bcrypt.compare(password, this.password);
};
 
module.exports = mongoose.model("User", userSchema);

In this model:


Setting Up Nodemailer

Configure Nodemailer with your email credentials.

config/email.js

const nodemailer = require("nodemailer");
 
const transporter = nodemailer.createTransport({
  service: "gmail", // Replace with your email provider
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS,
  },
});
 
module.exports = transporter;

Note: For Gmail, you may need to enable "Less secure app access" or use an app password if two-factor authentication is enabled.


Implementing Password Reset Routes

We’ll create two main routes for the password reset process:

  1. Request Password Reset: Sends a password reset email with a secure link.
  2. Reset Password: Verifies the token and updates the password.

Step 1: Request Password Reset Route

The first route handles password reset requests, generating a reset token and sending it via email.

routes/auth.js

const express = require("express");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const transporter = require("../config/email");
 
const router = express.Router();
 
// Generate password reset token
const generateResetToken = (user) => {
  return jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, { expiresIn: "1h" });
};
 
// Request password reset route
router.post("/request-password-reset", async (req, res) => {
  try {
    const { email } = req.body;
    const user = await User.findOne({ email });
    if (!user) return res.status(404).json({ message: "User not found" });
 
    const token = generateResetToken(user);
    const resetLink = `http://localhost:${process.env.PORT}/auth/reset-password?token=${token}`;
 
    // Send reset email
    await transporter.sendMail({
      from: process.env.EMAIL_USER,
      to: user.email,
      subject: "Password Reset Request",
      html: `<p>Click <a href="${resetLink}">here</a> to reset your password.</p>`,
    });
 
    res.status(200).json({ message: "Password reset email sent. Please check your inbox." });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});
 
module.exports = router;

In this code:

  1. The generateResetToken function creates a JWT with a 1-hour expiration.
  2. A password reset email containing the token is sent to the user’s email address with a reset link.

Step 2: Reset Password Route

The second route verifies the token and updates the user’s password if the token is valid.

routes/auth.js

// Reset password route
router.post("/reset-password", async (req, res) => {
  const { token, newPassword } = req.body;
  if (!token) return res.status(400).json({ message: "Token is required" });
 
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.id);
    if (!user) return res.status(404).json({ message: "User not found" });
 
    // Update password
    user.password = newPassword;
    await user.save();
 
    res.status(200).json({ message: "Password has been reset successfully" });
  } catch (error) {
    res.status(400).json({ message: "Invalid or expired token" });
  }
});

In this route:

  1. The server verifies the token using jwt.verify.
  2. If the token is valid, the user’s password is updated.

Protecting Sensitive Routes

For security, only verified users should be able to reset passwords. Ensure that you implement email verification (as described in a previous guide) or any other verification checks.


Testing the Password Reset Process

  1. Request a Password Reset: Send a POST request to /request-password-reset with the user’s email.
  2. Check the Email: Open the user’s inbox and find the password reset email.
  3. Click the Reset Link: Click the link in the email or copy it to your browser.
  4. Enter a New Password: Send a POST request to /reset-password with the token and newPassword.

Best Practices for Password Reset Security

  1. Set Expiration on Reset Tokens: Use short expiration times (e.g., 1 hour) to limit token misuse.
  2. Hash New Passwords: Ensure new passwords are hashed before saving them.
  3. Rate Limit Password Reset Requests: Implement rate limiting to prevent spam or brute-force requests.
  4. Notify Users of Password Changes: Send an email to notify users of a successful password reset, allowing them to secure their account if the reset was unauthorized.
  5. Use HTTPS in Production: Ensure tokens are transmitted securely over HTTPS.

Conclusion

Implementing password reset functionality in Node.js using JWT and Nodemailer is a secure and efficient way to handle forgotten passwords. By generating a JWT for the reset token and sending it via email, you can allow users to securely reset their passwords without compromising security.

This approach provides a seamless user experience while ensuring that only authorized users can update their passwords. Integrate these techniques into your project to offer a reliable and secure password reset process for your users.