Password Workflows With NodeJS on AWS
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.
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
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.
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.
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:
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.
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
The bulk of the logic is in the User.usePasswordToken()
function.
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.
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
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.