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
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 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;
- localhost:5000/reset?email=me@you.com&hash=122JNn2bi333
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
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?
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
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
Top comments (0)