Test Category

Test Blog Post

Starter template for writing out a blog post using MDX/JSX and Next.js.

No Name Exists

Abdullah Muhammad

Published on May 17, 20265 min read 1 views

Share:
Article Cover Image

Introduction

Please be advised that this article relies on material covered in the JWT Authentication article. If you find anything related to JWTs or back-end development confusing, please refer to this article before proceeding.

Last week, we went into detail on back-end authentication and learned about JWTs. Today, we will incorporate JWTs in another common use case and those are password resets.

When building out a web application, it is almost certain that some sort of email communication is required. Be it for account information, newsletters, orders, verification, and so on. Your email address is crucial to many of the services you use online.

Thankfully, there is a module out there that covers exactly that and we will be working with it to successfully create and send emails programmatically!

This is the diagram for this week, building on the web application built last week:

No Image Found
MERN stack incorporating JWT authentication, basic/protected routes, and Nodemailer

Nodemailer

So what is Nodemailer? It is a module for Node.js applications that makes creating and sending emails easy.

We are using Node.js for our back-end, so this is another piece of the puzzle that fits in seamlessly.

We could use Nodemailer for just about anything related to emails. But today specifically, we will look at password resets.

Whenever a user requests to reset their password, they must provide their email address. Often times, a token or link is sent to the email address with a specific expiry date and the user must verify themselves before resetting their old password.

That is what we will implement in the code overview and demo. We will incorporate Nodemailer, JWTs, UUID, and Bcryptjs to successfully create, secure and send emails containing an ID which the users must use to verify themselves before proceeding to reset their password.

For simplicity, we will use Gmail for this demo. Link to the Nodemailer module is here.


SMTP

Like many other protocols (HTTP/S, SSH, FTP, etc.), SMTP stands for Simple Mail Transfer Protocol and is a layer 7 protocol lying in the Application layer of the OSI (Open Systems Interconnection) model.

No Image Found
Open Systems Interconnection Model

To keep things simple, SMTP is a communication protocol for email transmission. Servers and other transfer devices can use SMTP to send and receive emails.

Similarly, how other application layer protocols have designated ports such as port 80 for HTTP or port 443 for HTTPS, email clients use SMTP on ports 465 or 587 for sending and receiving emails to/from a mail server.

When we use Nodemailer, we will first need to create a transport authenticating ourselves to a particular service (Gmail) which will use SMTP as the transfer protocol. Every mainstream email service on the web incorporates SMTP, it is a standard.


The Process

The approach to resetting passwords is fairly straight forward:

  1. User must enter email of a registered account to request password reset
  2. If email is invalid, alert is thrown, else an email containing the verification ID is sent with an expiry time of 5 minutes
  3. User must login to their email account and copy paste the verification ID
  4. User must paste this ID and fill in the new and confirm password fields on the reset portal
  5. User is notified if the password is successfully reset or not

That is all there is to it. We will continue to work with the User Posts web application built in the last demo and add a model tracking the verification IDs to the MongoDB collection.

Code Overview

The Github repository for this article is linked here. The directory we will focus on is /demos/Demo05_Nodemailer.

Building on the existing codebase from last week, a new model is added to the back-end and that is called EmailToken.

This model consists of two fields: email and token. It will be used to keep track of all the verification IDs sent via emails and ensure only one verification ID exists per user.

So if a user requests to reset password over and over, the old document stored in the collection is deleted and a new one is saved, ensuring there is only one valid token associated per user. A new email is also sent containing the new verification ID.

There are two new routes associated with this model and those are: /create-email-token and /delete-email-token and a new controller called EmailTokenController handles both of these.

When the user requests to reset password, they request /create-email-token. This route deals with verifying the email address, deleting any old EmailToken documents related to the email and creating a new one containing a new verification ID.

Once details are provided for the verification ID and new/confirm passwords, a /delete-email-token request is sent to verify if the correct ID was typed, update the PostUser collection with the new password, and delete the EmailToken document associated with email.

Here is the function that is mapped to the /create-email-token route:

GitHub GistJavaScript
require("dotenv").config({ path: '../.env' });
const bcryptjs = require("bcryptjs");
const EmailToken = require("../Model/EmailToken");
const jwt = require("jsonwebtoken");
const nodeMailer = require("nodemailer");
const PostUser = require("../Model/PostUser");
const UUID = require("uuid");

exports.createEmailToken = (req, res) => {
    const { email } = JSON.parse(req.body.body);

    // First check to see if the email requested for password reset is valid
    PostUser.find({ email }, (err, result) => {
        if (err) {
            res.status(400).json({
                message: "Could not traverse PostUser collection " + err
            });
        }
        else {
            // Invalid email
            if (result.length === 0) {
                res.status(401).json({
                    message: "No such email exists"
                });
            }
            else {
                const verification_ID = UUID.v4(); // Generate a random, unique ID

                // Salt & Hash this ID
                bcryptjs.genSalt(10, (err, salt) => {
                    if (err) {
                        res.status(400).json({
                            message: "Cannot salt ID " + err
                        });
                    }
                    else {
                        bcryptjs.hash(verification_ID, salt, (err, hashedID) => {
                            if (err) {
                                res.status(400).json({
                                    message: "Cannot hash password " + err
                                });
                            }
                            else {
                                // Delete any tokens associated with the account
                                EmailToken.deleteMany({ email }, (err, result) => {
                                    if (err) {
                                        res.status(400).json({
                                            message: "Could not clear any email tokens"
                                        });
                                    }
                                    else {
                                        // Sign JWT token with the hashed ID as payload and save to collection
                                        // JWT should only be valid for 5 minutes
                                        let emailTokenJWT = jwt.sign({ hashed_verification_id: hashedID } , process.env.TOKEN_SECRET, { expiresIn: 5 * 60 });
                                        let newEmailToken = new EmailToken({ email , token: emailTokenJWT });

                                        // Send email to user containing the actual verification ID 
                                        // Only be using GMAIL for this example
                                        const emailTransport = nodeMailer.createTransport({
                                            service: 'gmail',
                                            auth : {
                                                user: process.env.EMAIL_ADDRESS,
                                                pass: process.env.PASSWORD
                                            }
                                        });

                                        // Provide the destination and set it as the user's email along with text containing ID
                                        newEmailToken.save()
                                        .then(() => {                   
                                            let emailOptions = {
                                                from: process.env.EMAIL_ADDRESS,
                                                to: email,
                                                subject: 'Verification ID for password reset',
                                                text: `Here is the <b>Verification ID</b> needed to reset your password. 
                                                    ID will expire in <b>5 minutes:</b> ${verification_ID}`
                                              };

                                            // Send email containing details
                                            emailTransport.sendMail(emailOptions, (err, result) => {
                                                if (err){
                                                    res.status(400).json({
                                                        message: "Could not send email containing ID: " + err
                                                    });
                                                }
                                                else {
                                                    res.status(201).json({
                                                        message: "Successfully created/saved/send ID. " + result.response
                                                    });
                                                }
                                            });
                                        })
                                        .catch(err => {
                                            res.status(400).json({
                                                message: "Could not save email token to database " + err
                                            });
                                        });
                                    }
                                });
                            }
                        });
                    }
                });
            }
        }
    });
}
Create Email Token request is fulfilled with this function

After verifying that the email exists, we proceed to do the following:

  • UUID library is used to generate a random ID as the verification ID
  • The verification ID is hashed using the Bcryptjs library
  • Any previous EmailToken documents associated with user are deleted
  • A new JWT is signed with an expiry of 5 minutes and the hashed ID as the payload
  • The email and JWT are stored as a new document inside of the EmailToken collection
  • Transport is created using a built-in function from the Nodemailer module and Gmail is set as the service
  • Finally, an email is sent containing the original verification ID to the email requesting password reset using Nodemailer

"Wait, why are we using JWTs?"

They make the reset process easy. We want to set a time limit for how long the verification ID can be valid for. By saving them as a payload inside of a JWT, we can know for sure that if the JWT expires, the ID will also be invalid.

JWTs can be decoded. By ensuring the ID is hashed before storing it as a payload inside a JWT, we maintain its authenticity in case of a data breach.

"So in essence, the EmailToken collection saves only one JWT containing a payload of the hashed version of the verification ID associated with an account?"

Yes, that is all. The EmailToken collection saves a JWT to its token attribute.

Just below all that code, you will find the function dealing with /delete-email-token:

GitHub GistJavaScript
// Once User obtains verification ID, update password and delete email token from database
exports.deleteEmailToken = (req, res) => {
    const { email, ID, password } = JSON.parse(req.body.body);

    // First compare ID user submitted to one stored inside of the EmailToken collection
    // Check if email exists inside of collection
    EmailToken.find({ email }, (err, result) => {
        if (err) {
            res.status(400).json({
                message: "Cannot search EmailToken collection " + err
            });
        }
        else {
            let emailTokenJWT = result[0].token; // Extract the JWT stored inside the document

            jwt.verify(emailTokenJWT, process.env.TOKEN_SECRET, (err, payload) => {
                if (err) {
                    EmailToken.deleteOne({ email }, (err, result) => {
                        if (err) {
                            res.status(401).json({
                                message: "Token is expired and could not be removed " + err
                            });
                        }
                        else {
                            res.status(401).json({
                                message: "Token expired, deleted from EmailToken Collection!"
                            });
                        }
                    });
                }
                else {
                    // If JWT is valid (under 5 minutes), extract JWT payload and compare the ID to hashed ID
                    bcryptjs.compare(ID, payload.hashed_verification_id, (err, result) => {
                        if (err) {
                            res.status(400).json({
                                message: "Could not compare IDs"
                            });
                        }
                        // If comparison runs true, update password of user and delete email token
                        else if (result) {
                            // Update the password and stored it hashed and delete email token
                            bcryptjs.genSalt(10, (err, salt) => {
                                if (err) {
                                    res.status(400).json({
                                        message: "Could not generate a salt. " + err
                                    });
                                }
                                else {
                                    bcryptjs.hash(password, salt, (err, hashedPassword) => {
                                        if (err) {
                                            res.status(400).json({
                                                message: "Could not generate hash for new password. " + err
                                            });
                                        }
                                        else {
                                            // Once hash is generated for new password, save to PostUser collection
                                            // Delete EmailToken associated with email
                                            PostUser.updateOne( { email }, { $set : { password: hashedPassword }}, (err, result) => {
                                                if (err) {
                                                    res.status(400).json({
                                                        message: "Could not update document inside of PostUser collection. " + err
                                                    });
                                                }
                                                else {
                                                    // Now delete email token document from collection and send response
                                                    EmailToken.deleteOne({ email }, (err, result) => {
                                                        if (err) {
                                                            res.status(400).json({
                                                                message: "Could not delete EmailToken document after reset. " + err
                                                            });
                                                        }
                                                        else {
                                                            res.status(200).json({
                                                                message: "Password successfully reset! Email Token deleted."
                                                            });
                                                        }
                                                    });
                                                }
                                            });
                                        }
                                    });
                                }
                            });
                        }
                        else {
                            res.status(401).json({
                                message: "Invalid ID, password will not be reset"
                            });
                        }
                    });
                }   
            });
        }
    });
}
Delete Email Token request is fulfilled with this function

Once the user requests to reset after providing the verification ID and new password, this function completes the process as follows:

  • Check the EmailToken collection for the particular email and extract the JWT token
  • If JWT is not valid, the verification ID expired so respond with an error and delete the EmailToken document
  • If JWT is valid, decode the payload and compare the submitted verification ID to the hashed ID stored inside JWT
  • If comparison runs false, user entered an invalid ID and respond with an error
  • If comparison runs true, hash the new submitted password and update the document inside PostUser collection associated with the email and delete the EmailToken document associated with the email as well

And that is all! We are leveraging the power of many different libraries all at once and have incorporated Nodemailer to help us along the way!


Front-end

The only thing added to the existing codebase for the front-end is the Password Reset page:

GitHub GistJavaScript
import { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router';
import axios from 'axios';
import Alert from '../Alert/Alert';

const PasswordResetPage = () => {
    const [emailAddress, updateEmailAddress] = useState("");
    const [id, updateID] = useState("");
    const [newPassword, updatePassword] = useState("");
    const [confirmPassword, updateConfirmPassword] = useState("");
    const [alert, updateAlert] = useState("");
    const [emailCheck, updateEmailCheck] = useState(false);
    const [passwordCheck, updatePasswordCheck] = useState(false);
    const tokenValue = useSelector(state => state.auth.token);
    const navigate = useNavigate();
    
    // Check to see if the user is already logged in, redirect. User must not be logged in to password reset
    useEffect(() => {
        if (tokenValue){
            navigate("/")
        }
    }, []);

    const tokenFormHandler = (e) => {
        e.preventDefault();

        // Setting options to request email token
        let options = {
            method: 'POST',
            body: JSON.stringify({ email : emailAddress }),
            headers: {
                'content-type' : 'application/json'
            }
        };

        // Set email token when request is made
        axios.post('http://localhost:5000/create-email-token', options)
        .then(() => {
            updateAlert("success-valid-token");
            updateEmailCheck(true);
        })
        .catch(() => {
            updateEmailCheck(false);
            updateAlert("warning-invalid-token");
        });
    }

    const passwordResetHandler = (e) => {
        e.preventDefault();

        // If passwords do not match, throw alert
        if (newPassword !== confirmPassword) {
            updateAlert('warning-invalid-reset');
        }
        else {
            // Set options to make request to the backend
            let options = {
                method: 'POST',
                body: JSON.stringify({ email: emailAddress, ID: id, password: newPassword }),
                headers: {
                    'content-type' : 'application/json'
                }
            };

            // Request to delete email token after password is reset
            axios.post('http://localhost:5000/delete-email-token', options)
            .then(() => {
                updatePasswordCheck(true);
                updateAlert('success-password-reset');
            })
            .catch(() => {
                updatePasswordCheck(false);
                updateAlert('warning-invalid-reset');
            });
        }
    }

    return (
        <div className='password-reset-page'>
            <h1 style={{ marginTop: '1rem' }}><b>Password Reset</b></h1>
            <p><i>Enter in details to successfully reset password</i></p>
            { alert !== '' ? <Alert type={ alert } /> : null }
            <form style={{ marginLeft: 'auto', marginRight: 'auto', width: '50%' }} onSubmit={ tokenFormHandler }>
                <div className="mb-3">
                    <label className="form-label">Email address</label>
                    <input type="email" disabled={ emailCheck ? true : false } className="form-control" onChange={e => updateEmailAddress(e.target.value) } />
                </div>
                <button type="submit" disabled={ emailCheck ? true : false } className="btn btn-primary">Request Reset ID</button>
            </form>
            {
                emailCheck ? 
                    <>
                        <h1 style={{ marginTop: '5rem' }}>Reset your password</h1>
                        <p><i>Enter in the token ID, new password and confirm password to successfully reset it: </i></p>
                        <form style={{ marginLeft: 'auto', marginRight: 'auto', width: '50%' }} onSubmit={ passwordResetHandler }>
                            <div className="mb-3">
                                <label className="form-label">Email Reset Verification ID</label>
                                <input type="text" className="form-control" disabled={ passwordCheck ? true : false } onChange={ e => updateID(e.target.value) } />
                            </div>
                            <div className="mb-3">
                                <label className="form-label">Password</label>
                                <input type="password" className="form-control" disabled={ passwordCheck ? true : false } onChange={ e => updatePassword(e.target.value) } />
                            </div>
                            <div className="mb-3">
                                <label className="form-label">Confirm Password</label>
                                <input type="password" className="form-control" disabled={ passwordCheck ? true : false } onChange={ e => updateConfirmPassword(e.target.value) } />
                            </div>
                            <button style={{ marginBottom: '2rem' }} type="submit" disabled={ passwordCheck ? true : false } className="btn btn-success">Reset Password</button>
                        </form>

                    </>
                    : null
            }
        </div>

    )
}

export default PasswordResetPage;
PasswordResetPage.jsx containing options for resetting password

As you can see, the password reset has two forms: email token request and the actual password reset request.

The first form contains the email field and this must be submitted and verified before the second one is enabled to fill in the details for the ID and new/confirm passwords.

That is all!

Demo Time!

As always, we will be running two servers on two different ports for this demo. Port 5000 is where the Node server will be running and as usual, Port 3000 is where the client side code will run.

We will be using MongoDB Atlas for the database. Ensure to have your own separate .env file and provide details for the Node server port, database connection string and email address/email key for the Nodemailer transport setup.

Make sure to have a valid Gmail account and that account is setup to send emails using Nodemailer. This is the account that will send verification IDs to registered users and is the account provided inside createTransport().

If you are not clear how to setup a Gmail account for Nodemailer, please refer to this document.

All good? Upon launch, you should see this:

No Image Found
Home page of the User Posts site hosted on localhost:3000

Now, we proceed to register a user using their Gmail. If done correctly, you should see something like this (I have highlighted out the address except for the domain for security):

No Image Found
Register user complete using a Gmail account

Now we try to reset the password! Proceed to the Login page and select Reset Password and enter in the valid email address in the first form:

No Image Found
Providing the email address to request verification ID

If you entered the valid Gmail address of the registered account, the second form should become visible and an email containing a verification ID should be sent to the address. If done correctly, the email looks something like this:

No Image Found
Email sent using Nodemailer containing details about the verification ID

Notice how the header, sender and email body are all the things set up using Nodemailer’s built-in sendEmail() function.

Now, I did not hide the verification ID because by the time you will be reading this, it will already be expired lol ;)

Interestingly, check the EmailToken collection inside of MongoDB and you should see one token associated with this account:

No Image Found
Document exists inside of the EmailToken collection pertaining to the account requesting password reset

Interestingly, if you copy paste the token value to the official JWT site, you should see something like this:

No Image Found
Decoded payload of the JWT shows the hashed ID stored as the payload

You should see the same verification ID stored inside the JWT as payload except that it is hashed.

Quick! This token is only valid for five minutes so I will head over and fill out the second form using the verification ID and new password!

If you did yours correctly, you should see something like this:

No Image Found
Password successfully reset using verification ID provided in the email

If you check the EmailToken collection it should be empty and if you view the user in the PostUser collection it should be updated:

No Image Found
User is updated with their new password hashed

That is all! The demo is complete!

Conclusion

This article went into detail on sending emails using the Nodemailer package.

Hopefully, you saw how easy it was to setup a transport, incorporate a service and provide details to the type of email you would like to send.

We also covered the OSI model, SMTP and understood that mainstream services such as Gmail use these protocols for safe and secure email transfers.

We also incorporated JWTs, Bcryptjs, UUIDs along with Nodemailer to successfully reset user passwords.

Of course, there is a lot more you could do with Nodemailer, this was just one example.

I will link the Github repository here.

I hope you found this article helpful and look forward to more in the future.

Thank you!

No Name

Abdullah Muhammad

Blogger. Software Engineer. Designer.

Subscribe to the newsletter

Get new articles, code samples, and project updates delivered straight to your inbox.