DEV Community

Madhav Pandey
Madhav Pandey

Posted on • Edited on • Originally published at pblog.online

A Deep Dive into Password Reset Flows with Node.js and Express.js

blog-image
When it comes to web development and ensuring the security of user accounts, the password reset process is a critical component that should not be overlooked. Therefore, implementing a robust and secure password reset flow is a necessity for any web application that values user security and usability.

In this blog post, we will learn how to implement a robust password reset flow in your Node.js (Express.js) application. By the end, you'll have a comprehensive understanding of how to safeguard your users's accounts and provide them with a seamless password reset experience.

Below is the password reset flow diagram:

flow-diagram

Now, let's break down what steps are involved in this process one by one:

  1. Request for password reset: use can send a request for password reset by filling password reset form
  2. Valid user information: The system will validate user exists or not
  3. Send an email to the user with token: If the user exists, the system will send an email to the provided email ID with user id and a token which will expire within a certain time
  4. Check if the token is valid or expired: When the user clicks on the sent link, the system will again verify if the token is valid or not and if the token is related to that particular user or not
  5. Set the new password: If everything goes well, finally user will be given a password reset form where they can enter a new password

    1. Request for password reset Once the user clicks on Forgot password link the server will get get request and send a form where the user can enter their email id. The following code goes in router.js file.
 //password reset route
router.get("/forgot-pass",  (req, res)=>{
    res.render("reset")
 })

Enter fullscreen mode Exit fullscreen mode

Now, let's create reset.handlebars file where we will have the form

<div class="login-box">
 <div class="messages">
  {{>message}}
 </div>

  <form action="/password-reset" method="POST">
  <h2>Reset your password</h2>

    <div class="user-box">

      <input type="text" name="email" value="{{email}}" required>
      <label>Email address</label>
    </div>
<button type="submit">Send link</button>

  </form>

</div>
Enter fullscreen mode Exit fullscreen mode

password-reset-form

Once, the user enters their email id and hits Send link button the server will get post request at /password-reset endpoint.

  1. Validating user information and sending an email with a link

This is the most time-consuming part of this process. We have to verify user information, set up nodemailer, generate a token, and save the token in the database plus send an email with the token and user id. This all process happens when the server receive post requeset from /password-reset endpoint

First, let's create tokenSchema in token.js file to save token with user information and an expiration time of 1 hour.

const mongoose = require("mongoose");
const{Schema, model}= mongoose;

const tokenSchema= new Schema({
    userId:{
        type:Schema.Types.ObjectId,
        require:true,
        ref:"Users"   
    },
    token:{
        type:String,
        require:true
    },
    createdAt:{
        type:Date,
        default:Date.now,
        expires:3600
    }
})

module.exports = new model("token",tokenSchema);
Enter fullscreen mode Exit fullscreen mode

Now, let's create another file tokenHandeler.js file. Where we will write generating token, validating token and saving the token in database logic

const Token = require("../module/token");
const bcrypt = require("bcrypt");
const crypto = require("crypto");


const generateToken = async(id)=>{
let token = await Token.findOne({userId:id});
// delete already exits token
if(token){
    await Token.deleteOne({userId:id});
}

// generate token
const resetToken = crypto.randomBytes(32).toString("hex");

//hash reset token
const hashedToken =  await bcrypt.hash(resetToken,10)

// save token in database

await new Token({
    userId:id,
    token:hashedToken,
    createdAt:Date.now()
}).save()

return resetToken;

}



const isValidToken = async({token,id})=>{
    try{
        const savedToken = await Token.findOne({userId:id}).lean();
        //compare the token
        if(savedToken){
            const isValidToken = await bcrypt.compare(token,savedToken.token);
            return isValidToken;
        }else return false

    } catch(er){
        console.log("something went wrong..." )
    }

}



module.exports = {
    generateToken,
    isValidToken
}

Enter fullscreen mode Exit fullscreen mode

In the above code, we have generateToken function which will generate a unique token with the help of the crypto package. We hashed that token and save it to database along with user id and we have isValidToken function which will take token and id and compare it with saved token and userID

Let's set up nodemailer. For that create a file sendEmail.js. The file will have the following code:

const nodemailer = require("nodemailer");

const sendEmail = (payload)=>{
const { email, subject, html}= payload

const transporter = nodemailer.createTransport({
    host:process.env.EMAIL_HOST,
    secure:true,
    port:465,
    auth:{
        user:process.env.EMAIL_USERNAME,
        pass:process.env.EMAIL_PASSWORD
    }
})

const mailOptions ={
from:"p.blog@pblog.online",
to:email,
subject:subject,
html:  html
}

transporter.sendMail(mailOptions,(error,info)=>{
    if(error){
        console.log(error)
    }else{
console.log(info);
    }

})

}

module.exports = sendEmail;
Enter fullscreen mode Exit fullscreen mode

In the above code, we have sendEmail function. Inside that function we have set up nodemailer and using this function we can send an email to the user with the password reset link.

I have created a Zoho Mail Account. In my case host is smtp.zoho.com.au, user will be my Zoho mail and pass is Zoho application password.

Since we have set up nodemailer and token, now we are ready to handle post request came from /password-reset. In router.js file:

const express = require ("express");
const router = express.Router();
const sendEmail = require("./utils/sendEmail");
const {generateToken, isValidToken} = require ("./utils/tokenHandeler");
const flash = require("connect-flash");

// middleware fucntion to associate connect-flash on response
router.use((req,res,next)=>{
res.locals.message = req.flash();
next()
})

// password reset post route

router.post("/passport-reset", async (req,res)=>{
    const {email} = req.body;
    try{
let user = await Users.findOne({email});

if(user){
    const resetToken =  await generateToken(user._id);
    const link= `${req.protocol}://${req.get('host')}/password-reset-link?token=${resetToken}&id=${user._id}`;

// html for email
const html = `<b> Hi ${user.name}, </b>
<p> You requested to reset your password. </p>
<p> Please, click the link below to reset your password. </p>
<a href = "${link}"> Reset Password </a>
`

console.log(link);

const payload = {
    email,
    subject:"Password reset request",
    html
}

sendEmail(payload);


req.flash("success", "Check your email for the password reset link")
    res.redirect("/login")
}else{
    req.flash("error","We could not find any user, please check your email address again")
res.redirect("/forgot-pass")
}
    }catch(er){
        console.log(er);
        req.flash("error","Something went wrong, please try again later!")
        res.redirect("/forgot-pass")
    }
})

Enter fullscreen mode Exit fullscreen mode

Let's break it down above code. First, we looked for user associated with an email id provided by user. If user is not found then we have redirected user to /forgot-pass route with an error message. If user is found we generate a token using generateToken function and we create a dynamic link where we have token and user_id in the link. We send an email to the user by using sendEmail function. Then, we have redirected user to /login router with success message.

sent-reset-link

The password reset link will look like this:

http://localhost:8000/password-reset-link?token=e51d95cd5feba716fbe54163275ff874795fe7b52a625404a7e5adf00d3b2c22&id=62cd89d53e61de6c5bfbfe02

Enter fullscreen mode Exit fullscreen mode

On success, user will get an email where they can click to get the form where they can enter new password and send it back to server.

reset-link-email

I have used connect-flash to send message whenever a user is redirecting to specified route. I have created message.handlebars file inside partial folder and following code is in that file:

  {{#if message.success}}
  <div class="messages success-message">
      {{message.success}}
  </div>

    {{/if }}

   {{#if message.error}}
  <div class="messages error-message">
      {{message.error}}
  </div>
    {{/if }}
Enter fullscreen mode Exit fullscreen mode

This will show any error or success message to the user.

  1. Validating the token and sending password-reset form When user clicks on that reset link, server will get get request on /password-reset-link. Let's handle this route together :)
//password reset form route
router.get("/password-reset-link",  async (req,res)=>{

if(req.query && req.query.token && req.query.id){
    //check token and id are valid
const{token,id} = req.query;

try{
   const isValid = await isValidToken({token,id});
    if(isValid){
res.render("newPasswordForm",{
    token,
    id,

})
    }else{
res.json({message:"Invalid token or link is expired"})
    }

}catch(er){
    console.log(er)
res.json({message:"something went wrong, please try again latter"})
}


}else{
    res.redirect("/login")
}

})

Enter fullscreen mode Exit fullscreen mode

In the above code, we have extracted the token and user_id from the link. We validate token and user_id with the help of isValidToken function that we have created in tokenHandeler.js file. If the link is valid newPasswordForm is rendered and we have sent token and user_id as well. These are needed again when we get post request, once user enters new password and submit the form.

One important thing to remember here is that user need to click on that link within 1hour as our token expires in 1hour.
If user tried to modify the URL or click the link after 1hour, user would not be able to see the form and user will see an error message

error-message

Now, let's create a file newPasswordForm.handlebars file inside views folder.


<div class="main-wrapper">

<div class="login-box">
{{#if errorMessage}}
    <div class="messages error-message">
    {{errorMessage}}
    </div>
    {{/if}}
  <form action="/newPassword?token={{token}}&id={{id}}" method="POST">  
  <h2>Enter new password</h2>

<div class="user-box">
<input  type="password" name="password"  required>
<label > Password </label>
</div>

<div class="user-box">
<input type="password" name="repeatPassword"  required>
<label > Repeat Password </label>
</div>
<button type="submit"> Change Password</button>

  </form>

</div>

</div>

Enter fullscreen mode Exit fullscreen mode

This form will only shows up if the link is valid.

new-password-form

  1. Setting the new password Finally, we have reached to the final step of password reset process. Once the user clicks on the link and if the link is valid and not expired user can have access to new password form where they can enter new password and submit. Once form is submitted, server will get post request in /newPasswor route. Let's handle this post request.
//accept new password and save it to database
router.post("/newPassword",  async(req,res)=>{

    if(req.query.token && req.query.id){
const {token, id}= req.query;
let isValid;
try{

    isValid = await isValidToken({token,id});
}catch(er){
    console.log(er)
}
if(isValid){
const {password, repeatPassword}=req.body;

if(password.length<6){

   return  res.render("newPasswordForm",{
    token,
    id,
    errorMessage:"Password need to have minimum 6 characters"
   })
}
if (password!== repeatPassword){
   return  res.render("newPasswordForm",{
    token,
    id,
    errorMessage:" Password is not match."
   })
}


if(password == repeatPassword && password.length>6){
try{
    let hashedPassword = await bcrypt.hash(repeatPassword,10);
    let update_success = await Users.updateOne({_id:id},{password:hashedPassword});
    if(update_success){
        req.flash("success", "password is changed successfully.")
res.redirect("/login");
    }
}catch(er){
    console.log(er)
}
}
} else{
    res.json({message:"Invalid token or link is expired"})  

}

} else{
    res.json({message:"Something went wrong! try again latter"})  
}
    })

Enter fullscreen mode Exit fullscreen mode

Let's break down above code. Again, we have extracted token and user_id that came from req.query and we have validated that token one more time. If we have a valid token, we have extracted password and repeatPassword from req.body and we have validate the password. If the password is not valid we have render the same newPasswordForm with token, id and related errorMessage.
If the password requirement is matched, new password is hashed and saved it to the database. Finally, we redirect user to login page with success message.
password-changed-success

Now, user can login using their new password.

Thank you for taking the time to read this blog post, and I hope you find it valuable in your web development journey. If you have any questions or suggestions, please feel free to reach out. Happy coding!

Top comments (0)