Password Workflows With NodeJS on AWS

Nikolas Osvalds
The Startup
Published in
6 min readOct 19, 2020

--

Implementing a secure “forgot password” and “reset password” backend (for a Vue.js front end) in NodeJS with AWS Lambda, SES, and DynamoDB.

Background

I volunteer with DigitalHumani’s Reforestation as a Service API project which connects websites and mobile apps to trusted reforestation organizations to have trees planted for $1 when a user action is taken. The MVP was built by volunteers and continues to be maintained and improved by a small (but growing) team.

The team is working on a new secure dashboard website that will be used by clients organisations to view information about the number of trees their users have requested to be planted as well as by the DigitalHumani team to administer the API.

I tasked myself with designing and implementing the forgot password and reset password backends. A basic dashboard had already been created with the ability to register and log in using passport-jwt.

Workflow Steps

Descriptions in non technically detailed language to illustrate what we’re trying to implement.

Forgot Password

  • On login page, user clicks the forgot password link
  • They are taken to a form where they are prompted to enter their email and click submit
  • User receives an email with a URL
  • User navigates to the URL and enters new password (twice to confirm), then submits the form
  • The new password is saved and the user can login with their new password.

Reset Password

  • User logs in and navigates to the User Account page
  • User enters their existing password and new password (twice to confirm), then submits form
  • Password is updated for the user

Implementation

API Routes

POST /auth/forgot-passwordPOST /auth/reset-passwordPOST /auth/update-password

The backend of this application is implemented in NodeJS using the Express library. Here are the definitions in the main index.js file.

express route definitions for password workflows

POST /auth/forgot-password

The forgot-password route takes the user’s email address from the front end in a POST request and calls the AuthController.forgotPassword function.

Request format (JSON)

{
"email": "<email>"
}

The forgotPassword function does several things:

  • Validation that there is a user associated with the email provided
  • Expires any existing password reset tokens for the user to prevent old tokens from being used
  • Creates a new password reset token entry in the database
  • Generates an email with the URL that the user will use to reset their password
forgotPassword controller function

Password Reset Tokens w/ DynamoDB

So what are these password reset tokens I’m referring to? Well the idea here is that when the password reset request is made, you generate a new token entry in the database. This consists of a long random string and some metadata about the “token”. This long random string is then used on the /auth/reset-password route to ensure the request to update the password is legitimate.

Creating a random password reset token with NodeJS inbuilt crypto

In this case I decided to store the password reset token entries in the User table rather than using a separate DynamoDB table (a separate table seems common in the SQL world and is standard in frameworks like Laravel ). The benefits of a single table for DynamoDB in my case included a reduced number of queries to the database and less overhead of maintaining an additional table. You can read in great detail here about the benefits of single table design in DynamoDB.

JSON representation of a user record with password reset tokens in DynamoDB

In my case the User table was already defined from the previous work on the dashboard. I’ve pulled out the pertinent sections from the serverless.yml file here. Main points worth noting here are that the index.js is compiled into a Lambda function and the hash key for the user table is the email attribute.

Expiring existing password tokens

Working with DyanmoDB

I found that it was much easier to work with the data I needed to update as an object rather than trying to work with the DynamoDB UpdateExpressions to target a specific set of items. For example with expiring the existing password tokens, I grab the user.password_reset_token object and loop through that setting .used item to true to expire the tokens. Then update the entire user.password_reset_token item in DynamoDB.

Email components

The most important piece of the email is the URL that will take the user to the front end form to enter a new password. It must contain the correct domain and route of your front end as well as 2 query parameters, the email and the password reset token. The front end will need these parameters to send to the /auth/reset-password route along with the new password so that the backend can validate the token and update the password.

URL format and example

http(s)://domain/user/reset-password?token='<token>'&email='<email>'

https://my.digitalhumani.com/user/reset-password?token=af8ac9162927ee185c8b56c1c55b93117694ca1d355271c458e97c658f4d1d3a&email=email@domain.com

Populating the email

To fill the email with the dynamic data (e.g. url, username, expiration date, etc.) I used the Handlebars library to inject the emailData variables into the forgotPasswordEmail.html email template I based off of mailchimp’s opensource email-blueprints. In your html you wrap the variables you’d like to replace in curly braces {{ variable }}. See below for the body of the email:

body of forgot password email html template

Sending the Email

The parameters for the email are then built up in the params object in the required format for Amazon’s Simple Email Service SES. I won’t go into detail here as there are a lot of good tutorials out there on how to send emails with SES in NodeJS.

Example password reset email

POST /auth/reset-password

The reset-password endpoint takes the user’s email address, password reset token, and the new password from the front end in a POST request and calls the AuthController.resetPassword function.

Request format (JSON)

{
"email": "<email>",
"token": "<token>",
"password1": "<password1>",
"password2": "<password2>"
}

This function does several things:

  • Validation of the new password
  • Looks up token provided in the User table and consumes it if valid
  • Sets the new password for the user
reset password controller function

The bulk of the logic is in the User.usePasswordToken() function.

function to validate and consume a password token

The hashed password is generated using a utility function which utilizes the bcryptjs library.

At this point the user has now successfully updated their password!

Now what if they haven’t forgotten their password and just want to update it to something more secure or they suspect it’s been compromised.? That’s where the update-password route comes in.

POST /auth/update-password

The update-password endpoint takes the user’s email address, current password, and new password (twice to confirm they match) from the front end in a POST request and calls the AuthController.updatePassword function.

express route definitions for password workflows

You may have noticed the additional argument provided in this route passport.authenticate(‘jwt’, { session: false }). This is passport JWT middleware which will enforce the requirement that a user be in a valid authenticated dashboard session to change their password.

Request format (JSON)

{
"email": "<email>",
"currentPassword": "<current password>",
"password1": "<password1>",
"password2": "<password2>"
}

This function does several things:

  • Validates that there is a user associated with the email provided
  • Validates the current password is correct for the user
  • Validates the new password format and that they match
  • Sets the new password for the user
controller that updates the password when a user is authenticated
password comparison function using bcryptjs

With this complete we have a functioning backend for all of our password reseting and updating workflows. I hope this helped you with making your own password workflows with NodeJS and DynamoDB!

Further Improvement Ideas

  • Create a cron job to clean up old/expired password reset token entries
  • Design a report that uses the data stored in the password reset token entries

Resources

I used this great article as a guide, but was pretty on my own for the DynamoDB pieces.

Additionally OWASP has a nice Forgot Password cheat sheet that was relevant.

If you’re interested in learning the serverless framework I’d highly recommend Complete Coding’s video series.

Check out my other projects

--

--

Nikolas Osvalds
The Startup

I’m passionate about doing good, giving back, and helping to tackle the climate crisis with my working life, ideally with code.