Handle Password Reset in ExpressJS

Search for a command to run...

No comments yet. Be the first to comment.
In this series, we will build a basic social application using ExpressJS, MongoDB.
If you’re building an application, you likely want a lot of users on your platform. However, you don’t just want a large number of users, you want real and high-quality users who will interact with your app. You want to verify those users. It’s commo...
I started working on an open-source project about a year ago to build an embeddable alternative to Redis in Go. That’s when I started building the EchoVault project. Since then, the project has gained some interest and even a few contributors. This a...

AWS Cognito user pools allow you to manage your app's within the AWS ecosystem. It provides all the basic features you'd expect from an auth system. When I'm building an application on AWS infrastructure, I prefer using Cognito user pools due to its ...

AWS offers an easy way to work with web sockets through the WebSockets API in API Gateway. This article is going to be a short walkthrough on how to get started with WebSocket APIs. Setup This is article is based on a serverless framework project. Yo...

I've previously written an article about basic username/password authentication with AWS Cognito. This article will cover registration and authentication using Google. Cognito offers this functionality built into the hosted-ui. However, if you find ...

Recently, I posted an article about pre signup triggers for AWS Cognito user pools. The article went over setting up pre signup validation using a lambda function. In this article, I'm going to demonstrate how to achieve the same goal, but using the ...

No authentication system is complete without a password reset feature. I would personally never ship a product that did not have this feature included.
It is necessary to provide a way for users to recover access to their accounts/data in case of a lost or forgotten password. In this article, I will be demonstrating how to handle password resets in ExpressJS.
In the last 2 articles, I wrote about how to connect ExpressJS application to MongoDB database and building a user registration and authentication system.
Both of these articles tie into today’s article. We’re going to use mongoose and our saved user data to enable password resets.
If you’ve read those articles, or already have your own authentication system, read on. Even if you’re using a different tech stack, you may still gain some valuable ideas from this approach.
As always, this project is hosted on Github. Feel free to clone the project to get access to the source code I use in this article.
Before we dive into the code, let’s first establish what the password reset flow will look like from the user’s perspective and then design the implementation of this flow.
From the user’s perspective, the process should go as follows:
We also need to understand some characteristics of a good password reset system:
We now have a clear picture of the reset flow from the user’s perspective and the characteristics of a password reset system. Here are the steps we will take in the implementation of this system:
Firstly, we’ll have to set up the project. Install the uuid package for generating a unique token, and the nodemailer package for sending emails.
npm install uuid nodemailer
Add the full domain to the environment variables. We’ll need this in order to generate a fill link string to email to the user.
DOMAIN=http://localhost:8000
Make some changes to the app entry file in the following areas:
useCreateIndex to ‘true’ in the mongoose connection options. This makes mongoose’s default index build use createIndex instead of ensureIndex and prevents MongoDB deprecation warnings.const connection = mongoose.connect(process.env.MONGO\_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
})
...
app.use('/', require('./routes/password-reset'))
We need to have a dedicated model to handle the password reset records. In the models folder, create a model called ‘PasswordReset’ with the following code:
const { Schema, model } = require('mongoose')
const schema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
token: {
type: Schema.Types.String,
required: true
}
}, {
timestamps: true
})
schema.index({ 'updatedAt': 1 }, { expireAfterSeconds: 300 })
const PasswordReset = model('PasswordReset', schema)
module.exports = PasswordReset
We have two properties in this model, the user that’s requested the password reset, and the unique token assigned to the particular request.
Make sure to set the timestamps option to true in order to include createdAt and updatedAt fields in the document.
After defining the schema, create an index on the updatedAt field with an expiry time of 300 seconds (5 minutes). I’ve set it this low for testing purposes. In production, you can increase this to something more practical like 2 hours.
In the User model we created in this article (or the user model you currently have), update the pre save hook to the following:
userSchema.pre('save', async function(next){
if (this.isNew || this.isModified('password')) this.password = await bcrypt.hash(this.password, saltRounds)
next()
})
Do this to make sure the password field is hashed whether the document is new or the password field has been changed in an existing document.
Create a new file in the route’s folder called ‘password-reset.js’. This is the file we import in the app entry file.
In this file, import the User and PasswordReset models. Import the v4 function from the uuid package for token generation.
const router = require('express').Router()
const { User, PasswordReset } = require('../models')
const { v4 } = require('uuid')
/* Create routes here */
module.exports = router
Create the first 2 routes. These routes are associated with the form which accepts the user’s email address.
router.get('/reset', (req, res) => res.render('reset.html'))
router.post('/reset', async (req, res) => {
/* Flash email address for pre-population in case we redirect back to reset page. */
req.flash('email', req.body.email)
/* Check if user with provided email exists. */
const user = await User.findOne({ email: req.body.email })
if (!user) {
req.flash('error', 'User not found')
return res.redirect('/reset')
}
/* Create password reset token and save in collection along with user.
If there already is a record with current user, replace it. */
const token = v4().toString().replace(/-/g, '')
PasswordReset.updateOne({
user: user._id
}, {
user: user._id,
token: token
}, {
upsert: true
})
.then( updateResponse => {
/* Send email to user containing password reset link. */
const resetLink = `${process.env.DOMAIN}/reset-confirm/${token}`
console.log(resetLink)
req.flash('success', 'Check your email address for the password reset link!')
return res.redirect('/login')
})
.catch( error => {
req.flash('error', 'Failed to generate reset link, please try again')
return res.redirect('/reset')
})
})
The first is a GET route to ‘/reset’. In this route, render the ‘reset.html’ template. We will create this template later.
The second route is a POST route for ‘/reset’. This route expects the user’s email in the request body. In this route:
At the moment, we’re only logging the link to the console. We will implement the email logic later.
Create the 2 routes that come into play when the user visits the link generated link above.
router.get('/reset-confirm/:token', async (req, res) => {
const token = req.params.token
const passwordReset = await PasswordReset.findOne({ token })
res.render('reset-confirm.html', {
token: token,
valid: passwordReset ? true : false
})
})
router.post('/reset-confirm/:token', async (req, res) => {
const token = req.params.token
const passwordReset = await PasswordReset.findOne({ token })
/* Update user */
let user = await User.findOne({ _id: passwordReset.user })
user.password = req.body.password
user.save().then( async savedUser => {
/* Delete password reset document in collection */
await PasswordReset.deleteOne({ \_id: passwordReset.\_id })
/* Redirect to login page with success message */
req.flash('success', 'Password reset successful')
res.redirect('/login')
}).catch( error => {
/* Redirect back to reset-confirm page */
req.flash('error', 'Failed to reset password please try again')
return res.redirect(`/reset-confirm/${token}`)
})
})
The first route is a get route that expects the token in the url. The token is extracted and then validated. Validate the token by searching the PasswordReset collection for a document with the provided token.
If the document is found, set the ‘valid’ template variable to true, otherwise, set it to false. Be sure to pass the token itself to the template. We will use this in the password reset form.
Check the validity of the token by searching the PasswordReset collection by token.
The second route is a POST route that accepts the password reset form submission. Extract the token from the url and then retrieve the password reset document associated with it.
Update the user associated with this particular password reset document. Set the new password and save the updated user.
Once the user is updated, delete the password reset document to prevent it from being reused to reset the password.
Flash a success message and redirect the user to the login page where they can log in with their new password.
If the update is unsuccessful, flash an error message and redirect back to the same form.
Once we’ve created the routes, we need to create the templates
In the views folder, create a ‘reset.html’ template file with the following content:
{% extends 'base.html' %}
{% set title = 'Reset' %}
{% block styles %}
{% endblock %}
{% block content %}
<form action='/reset' method="POST">
{% if messages.error %}
<div class="alert alert-danger" role="alert">{{ messages.error }}</div>
{% endif %}
<div class="mb-3">
<label for="name" class="form-label">Enter your email address</label>
<input
type="text"
class="form-control {% if messages.error %}is-invalid{% endif %}"
id="email"
name="email"
value="{{ messages.email or '' }}"
required>
</div>
<div>
<button type="submit" class="btn btn-primary">Send reset link</button>
</div>
</form>
{% endblock %}
Here we have one email field that is pre-populated with an email value if one was flashed in the previous request.
Include an alert that displays an error message if one has been flashed from the previous request.
Create another template in the same folder named ‘reset-confirm.html’ with the following content:
{% extends 'base.html' %}
{% set title = 'Confirm Reset' %}
{% block content %}
{% if not valid %}
<h1>Oops, looks like this link is expired, try to <a href="/reset">generate another reset link</a></h1>
{% else %}
<form action='/reset-confirm/{{ token }}' method="POST">
{% if messages.error %}
<div class="alert alert-danger" role="alert">{{ messages.error }}</div>
{% endif %}
<div class="mb-3">
<label for="name" class="form-label">Password</label>
<input
type="password"
class="form-control {% if messages.password_error %}is-invalid{% endif %}"
id="password"
name="password">
<div class="invalid-feedback">{{ messages.password_error }}</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Confirm password</label>
<input
type="password"
class="form-control {% if messages.confirm_error %}is-invalid{% endif %}"
id="confirmPassword"
name="confirmPassword">
<div class="invalid-feedback">{{ messages.confirm_error }}</div>
</div>
<div>
<button type="submit" class="btn btn-primary">Confirm reset</button>
</div>
</form>
{% endif %}
{% endblock %}
In this form, check for the value of the ‘valid’ variable that we set in the GET route, if false, render the expired token message. Otherwise, render the password reset form.
Include an alert that displays an error message if one was flashed in the previous request.
Go to the login form that we created in the registration & authentication article and add the following code to the top of the form:
{% if messages.success %}
<div class="alert alert-success" role="alert">{{ messages.success }}</div>
{% endif %}
This renders the success messages that we flash when we create/send the reset link and when we update the user’s password before redirecting to the login page.
In the previous routes section, we logged the reset link in the console. Ideally, we should send an email to the user when they’ve requested a password reset link.
For this example, I’ve used ethereal.email to generate a test email account for development purposes. Head over there and create one (it’s a one-click process).
Once you’ve created the test account, add the following variables to your environment variables:
EMAIL_HOST=smtp.ethereal.email
EMAIL_NAME=Leanne Zulauf
EMAIL_ADDRESS=leanne.zulauf@ethereal.email
EMAIL_PASSWORD=aDhwfMry1h3bbbR9Av
EMAIL_PORT=587
EMAIL_SECURITY=STARTTLS
These are my values at the time of writing, plug in your own values here.
Create a ‘helpers.js’ file in the root of the project. This file will have a bunch of useful functions that are likely to be reused across the entire project.
Define these functions here so that we can import them when they’re needed rather than repeating similar logic all over our application.
const nodemailer = require('nodemailer')
module.exports = {
sendEmail: async ({ to, subject, text }) => {
/* Create nodemailer transporter using environment variables. */
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: Number(process.env.EMAIL_PORT),
auth: {
user: process.env.EMAIL_ADDRESS,
pass: process.env.EMAIL_PASSWORD
}
})
/* Send the email */
let info = await transporter.sendMail({
from: `"${process.env.EMAIL_NAME}" <${process.env.EMAIL_ADDRESS}>`,
to,
subject,
text
})
/* Preview only available when sending through an Ethereal account */
console.log(`Message preview URL: ${nodemailer.getTestMessageUrl(info)}`)
}
}
Export an object with various functions. The first being the sendEmail function.
This function takes the recipient’s address, email subject and email text. Create the NodeMailer transporter, using the environment variables defined previously in the options. Send the email using the arguments passed to the function.
The last line of the function logs the message url in the console so you can view the message on Ethereal mail. The test account does not actually send the email.
Go back to the ‘password-reset.js’ routes and add the email functionality. First, import the function:
const { sendEmail } = require('../helpers')
In the ‘/reset’ POST route, instead of logging the reset link on the console, add the following code:
sendEmail({
to: user.email,
subject: 'Password Reset',
text: `Hi ${user.name}, here's your password reset link: ${resetLink}.
If you did not request this link, ignore it.`
})
Send an additional email to notify the user of a successful password change in the ‘/reset-confirm’ POST route once the user is successfully updated:
user.save().then( async savedUser => {
/* Delete password reset document in collection */
await PasswordReset.deleteOne({ _id: passwordReset._id })
/* Send successful password reset email */
sendEmail({
to: user.email,
subject: 'Password Reset Successful',
text: `Congratulations ${user.name}! Your password reset was successful.`
})
/* Redirect to login page with success message */
req.flash('success', 'Password reset successful')
res.redirect('/login')
}).catch( error => {
/* Redirect back to reset-confirm page */
req.flash('error', 'Failed to reset password please try again')
return res.redirect(`/reset-confirm/${token}`)
})
In this article, I demonstrated how to implement a password reset feature in ExpressJS using NodeMailer.
In the next article, I will write about implementing a user email verification system in your Express application. I will use a similar approach to the one used in this article, with NodeMailer being the email package of choice.