How to create a secure password reset in JavaScript?

Content verified by Anycode AI
July 21, 2024
Security related to passwords is of absolute importance in today's digital world. The most common problem of any user accessing a web application is that he forgets his password. Therefore, a robust, secure, and easy method for password reset is a must for any web application. We demonstrate in this tutorial the password reset functionality with advanced security using JavaScript, Node.js, Express, MongoDB, and JSON Web Tokens. This would be for ensuring a secure password reset by a user by sending a secure token to the email address of the user. The step-by-step guide covers the frontend and backend components, mentioning best practices to help in keeping security and usability in line. At the end of this guide, a developer is better equipped with the enhancement of security and user experience of their application.

Step 1: Setup the Project

Start by creating a new Node.js project in any directory you prefer:

mkdir secure-password-reset
cd secure-password-reset
npm init -y
npm install express bcrypt jsonwebtoken dotenv body-parser

You'll want a structure like this:

secure-password-reset/
  ├── server/
  │   ├── node_modules/
  │   ├── config/
  │   │   └── database.js
  │   ├── routes/
  │   │   └── auth.js
  │   ├── controllers/
  │   │   └── authController.js
  │   ├── models/
  │   │   └── user.js
  │   ├── .env
  │   ├── package.json
  │   ├── app.js

Database Connection

Hook up your database in config/database.js. For this, we’re going with MongoDB, so don't forget to install mongoose:

npm install mongoose

And here’s config/database.js:

const mongoose = require('mongoose');
require('dotenv').config();

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB connected');
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }
};

module.exports = connectDB;

User Model

Create a user model with email, password, and a reset token in models/user.js.

models/user.js:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  resetToken: String, 
  resetTokenExpiration: Date,
});

UserSchema.pre('save', async function (next) {
  if (!this.isModified('password')) {
    return next();
  }
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

module.exports = mongoose.model('User', UserSchema);

Create Authentication Routes

Set up your routes in routes/auth.js.

routes/auth.js:

const express = require('express');
const { register, login, requestPasswordReset, resetPassword } = require('../controllers/authController');
const router = express.Router();

router.post('/register', register);
router.post('/login', login);
router.post('/request-password-reset', requestPasswordReset);
router.post('/reset-password', resetPassword);

module.exports = router;

Authentication Controller

The controller with registration and password reset logic.

controllers/authController.js:

const User = require('../models/user');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
require('dotenv').config();

const transporter = nodemailer.createTransport({
  service: 'Gmail',
  auth: {
    user: process.env.EMAIL,
    pass: process.env.EMAIL_PASSWORD,
  },
});

exports.register = async (req, res) => {
  const { email, password } = req.body;
  
  try {
    let user = await User.findOne({ email });
    if (user) {
      return res.status(400).json({ msg: 'User already exists' });
    }
    user = new User({ email, password });
    await user.save();
    res.status(201).json({ msg: 'User registered' });
  } catch (err) {
    console.error(err.message);
    res.status(500).send('Server error');
  }
};

exports.login = async (req, res) => {
  const { email, password } = req.body;

  try {
    let user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ msg: 'Invalid credentials' });
    }
    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
      return res.status(400).json({ msg: 'Invalid credentials' });
    }
    const payload = { user: { id: user.id } };
    jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' }, (err, token) => {
      if (err) throw err;
      res.json({ token });
    });
  } catch (err) {
    console.error(err.message);
    res.status(500).send('Server error');
  }
};

exports.requestPasswordReset = async (req, res) => {
  const { email } = req.body;

  try {
    let user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ msg: 'User not found' });
    }

    const resetToken = crypto.randomBytes(32).toString('hex');
    user.resetToken = resetToken;
    user.resetTokenExpiration = Date.now() + 3600000; // 1 hour
    await user.save();

    const resetUrl = `http://localhost:3000/reset-password/${resetToken}`;
    transporter.sendMail({
      to: user.email,
      from: 'noreply@test.com',
      subject: 'Password Reset',
      html: `<p>You requested a password reset</p><p>Click this <a href="${resetUrl}">link</a> to set a new password.</p>`
    });

    res.status(200).json({ msg: 'Reset link sent to email' });

  } catch (err) {
    console.error(err.message);
    res.status(500).send('Server error');
  }
};

exports.resetPassword = async (req, res) => {
  const { token, newPassword } = req.body;

  try {
    let user = await User.findOne({ resetToken: token, resetTokenExpiration: { $gt: Date.now() } });
    if (!user) {
      return res.status(400).json({ msg: 'Invalid or expired token' });
    }

    user.password = newPassword;
    user.resetToken = undefined;
    user.resetTokenExpiration = undefined;
    await user.save();

    res.status(200).json({ msg: 'Password reset successfully' });

  } catch (err) {
    console.error(err.message);
    res.status(500).send('Server error');
  }
};

Bootstrap the Application

Set up Express and include middleware to parse request bodies.

app.js:

const express = require('express');
const connectDB = require('./config/database');
const authRoutes = require('./routes/auth');
const bodyParser = require('body-parser');
require('dotenv').config();

const app = express();

// Connect to database
connectDB();

// Middleware
app.use(bodyParser.json());

// Define routes
app.use('/api/auth', authRoutes);

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));

Environment Configuration

Don’t forget the .env file in your server directory, which holds environment-specific settings:

.env:

MONGO_URI=mongodb://localhost:27017/secure-password-reset
JWT_SECRET=your_jwt_secret_key
EMAIL=your_email@gmail.com
EMAIL_PASSWORD=your_email_password

This configuration gives you a robust password reset mechanism. Key security measures:

  1. Password Hashing: Using bcrypt to hash passwords before saving.
  2. JWT for Authentication: JWT tokens for validating user sessions.
  3. Secure Token for Reset: Time-limited tokens generated with crypto.
  4. Transport Layer Security: Use HTTPS to safeguard data during transmission.

There's always room for improvement, especially in production. Think about rate limiting, sophisticated validation, account lockouts, and advanced monitoring of security practices to make the platform even more secure.

Have any questions?
Our CEO and CTO are happy to
answer them personally.
Get Beta Access
Anubis Watal
CTO at Anycode
Alex Hudym
CEO at Anycode