How To Implement A Password Reset Feature In Your NodeJS Application

How To Implement A Password Reset Feature In Your NodeJS Application

Learn How You can Add A Password Reset Feature To Your NodeJS Application

ยท

8 min read

In my article today, I will show you how you can implement a fully functional password reset feature in your NodeJS application.

GETTING STARTED

THE CRYPTO MODULE

We will be using the built-in node module crypto to generate a hash for the password reset link. If this module does not come with your node version, use the command below to install it.

$ npm i crypto

If you have installed the module / verified that the module exists already, we may proceed with the next step

THE MODEL FILE

Head over to the user model and add the methods below to the schema object.

//generate password reset hash
userSchema.methods.generatePasswordResetHash = function(){
    //create hash object, then create a sha512 hash of the user's current password 
    //and return hash
    const resetHash = crypto.createHash('sha512').update(this.password).digest('hex')
    return resetHash;
}

//verify password reset hash
userSchema.methods.verifyPasswordResetHash = function(resetHash = undefined){
    //regenerate hash and check if they are equal
    return this.passwordResetHash() === resetHash;
}

THE FIRST METHOD

This method generatePasswordResetHash() will generate and return a sha512 hash using the user's current password.

If in your database, the user's password is hashed already, you must modify the method to use the hashed password instead.

So you will do something like this

//generate password reset hash
userSchema.methods.generatePasswordResetHash = function(){
    //create hash object, 
    //then create a sha512 hash of the user's hashed password 
    //and then return hash
    const resetHash = crypto.createHash('sha512').update(this.hash).digest('hex')
    return resetHash;
}

So if you want to use this method, create a new instance of the model and then pass in the user's document as an argument so that it will extract the user's hashed password or the user's password (if you stored it in plain text)

THE SECOND METHOD

This method verifyPasswordResetHash() will regenerate the hash again and then compare it against the submitted hash from the URL.

Now you need to set up the HTML pages needed for this feature. If you have one set up already, you may skip the step below.

THE HTML TEMPLATES

reset.html

This HTML page will have a form that will send the password reset link to the user.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reset Your Password</title>
    <!-- Google Fonts -->
    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
    <!-- MDB -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/5.0.0/mdb.min.css" rel="stylesheet" />

    <style>
        .body{
            font-family: "Roboto", sans-serif;
        }
        .container{
            box-shadow: 0px 0px 8px #ddd
        }
    </style>
</head>

<body>
    <section class="container p-4 mt-5" style="max-width:500px">
        <h4 class="text-center mb-4">RESET YOUR PASSWORD</h4>
        <form method="post" action="/reset" style="max-width:500px;">
            <div class="mb-3">
                <label class="form-label">Email Address</label>
                <input type="email" name="email" class="form-control" placeholder="simon@webdev.com">
            </div>
            <div class="mb-3">
                <button type="submit" class="btn btn-primary">Send Reset Link</button>
            </div>
        </form>
    </section>
</body>

</html>

new_pass.html

This HTML page will have a form that will enable the user to enter new password.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Enter New Password</title>
    <!-- Google Fonts -->
    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
    <!-- MDB -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/5.0.0/mdb.min.css" rel="stylesheet" />

    <style>
        .body{
            font-family: "Roboto", sans-serif;
        }
        .container{
            box-shadow: 0px 0px 8px #ddd
        }
    </style>
</head>

<body>
    <section class="container p-4 mt-5" style="max-width:500px">
        <h4 class="text-center mb-4">ENTER NEW PASSWORD</h4>
        <form method="post" action="/reset-pass" style="max-width:500px;">
            <div class="mb-3">
                <label class="form-label">New password</label>
                <input type="password" name="pass" class="form-control" placeholder="*****">
            </div>
            <div class="mb-3">
                <label class="form-label">Re-enter password</label>
                <input type="password" name="conpass" class="form-control" placeholder="*****">
            </div>
            <div class="mb-3">
                <button type="submit" class="btn btn-success">Reset</button>
            </div>
        </form>
    </section>
</body>

</html>

So this is an example of a password reset request form

image.png

Now here is the logic ๐Ÿ˜ƒ

When a user enters his email address and requests for a password reset link, the system will check if the email address exists.

If the email address exists, the application should generate a hash using the user's current password or the user's hashed password if the password is hashed before they are saved, then I will attach this hash with the user's email address to the password reset link

But if the email address does not exist, the application should respond with the message "Email address does not exist".

Now, when the user opens the password reset link in a browser, we will extract the user's email address and the hash from the URL. Then we will check again if the email address exists and if the hash is valid.

If the hash is valid, we will then issue a password reset form.

THE SERVER FILE

Now let us create a POST route that will generate a password reset link and send it to the user's email address.

In our password reset link, we will attach an Identifier that will help us to identify the particular user that is requesting for a password reset. Our Identifier in this case is the user's email address, and when we generate a password reset link for the user, it will look like the format below;

Getting interesting right?

Let's code it

In your server file, add the code below

app.post('/reset', async (req, res) => {
    try{
        //find a document with such email address
        const user = await User.findOne({email : req.body.email})
        //check if user object is not empty
        if(user){
            //generate hash
            const hash = new User(user).generatePasswordResetHash()
            //generate a password reset link
            const resetLink = `http://localhost:5000/reset?email=${user.email}?&hash=${hash}`
            //return reset link
            return res.status(200).json({
                resetLink
            })
            //remember to send a mail to the user
        }else{
            //respond with an invalid email
            return res.status(400).json({
                message : "Email Address is invalid"
            })
        } 
    }catch(err){
        console.log(err)
        return res.status(500).json({
            message : "Internal server error"
        })
    }
})

This is the result of the code above, when I submit my email address for a password reset link

image.png

At this stage, you only need to send a mail to the user's email address with the password reset link.

A LITTLE EXPLANATION

Let's talk about the code above.

Remember that we added 2 schema methods in our model file.

The first method generatePasswordResetHash() used above, generates a hash with the user's current password using the sha512 algorithm and this hash will be included in our password reset link.

So what I basically did, was to create a new instance of the model and I passed in the user's document as an argument so that the method generatePasswordResetHash() can take in the password directly when it is invoked.

WHAT HAPPENS NEXT?

Now, what happens when the user clicks on the reset link?

i-dont-know-what-happened.gif

That's a good question.

We need to set up / modify the already existing GET route for /reset.

Recall that this route serves a form that requests an email address from the user for the password reset link to be sent to.

We will check if the URL has a query that contains the email address & the hash, then we will extract them. This means that the URL must be in the format localhost:5000/reset?email=me@you.org&hash=aws5e5c44

//reset route
app.get('/reset', async (req, res) => {
    try {
        //check for email and hash in query parameter
        if (req.query && req.query.email && req.query.hash) {
            //find user with suh email address
            const user = await User.findOne({ email: req.query.email })
            //check if user object is not empty
            if (user) {
                //now check if hash is valid
                if (new User(user).verifyPasswordResetHash(req.query.hash)) {
                    //save email to session
                    req.session.email = req.query.email;
                    //issue a password reset form
                    return res.sendFile(__dirname + '/views/new_pass.html')
                } else {
                    return res.status(400).json({
                        message: "You have provided an invalid reset link"
                    })
                }
            } else {
                return res.status(400).json({
                    message: "You have provided an invalid reset link"
                })
            }
        } else {
            //if there are no query parameters, serve the normal request form
            return res.sendFile(__dirname + '/views/reset.html')
        }
    } catch (err) {
        console.log(err)
        return res.status(500).json({
            message: "Internal server error"
        })
    }
})

A LITTLE EXPLANATION

Let's talk about the code above.

Recall again that we added 2 schema methods in our model file.

The second method verifyPasswordResetHash() used above, regenerates the hash with the user's current password, then compares this hash with the one from the URL. It returns true if they are equal or false if they are not.

So what I basically did was to create a new instance of the model and pass in the current user's document as an argument, then I invoked the method verifyPasswordResetHash() and passed in the hash from the URL as an argument, for the method to regenerate a hash and compare to see if they are equal.

What happens if they are equal?

It means that the user truly requested for a password reset and what we need to do now is to save the user's email address in the session and issue a password reset form.

So this is a sample page that will be rendered when the hash is valid

image.png

All you have to do now is;

  • Create a new route that will read the form data
  • Compare the two passwords
  • Then update the user's password using his email address stored in the session

After the user must have updated his password, the password reset link will become invalid because the hash will no longer be the same.

EXTRA

I published an e-book that will help you understand how to perform CRUD operations in MongoDB Using NodeJS And Express. If you are a fan of my articles and you love how I write, I am sure that you will love this e-book too.

Use the link below to get $5 off your purchase.

Here is a Github repository implementing a basic password reset feature in NodeJS. Check it out and modify it to suit your project.

Thank you for reading

Image Credit: Freepik

ย