Forgot Password using Node JS with Knex and Express
A detailed analysis of how to make a reset for a user when he has forgotten his password. How to send email from Node JS and validate sending messages.
Most of us have experienced the account recovery process at least once — when we forget a password, there’s a need for procedures to create a new one and regain access to the system. This article focuses on implementing such a process using Node.js, Knex, and some undisclosed tools, alongside Express to handle routes and perform the necessary operations. We’ll cover the router implementation, handling URL parameters, determining what to send to the user when only an email or phone number is available as proof, managing email submissions, and addressing security concerns.
Forgot Password Flow
Before diving into coding, I’d like to ensure that we’re working with the same codebase, which you can access from my public repository on GitHub. We will upgrade step by step to implement the forgot password flow. For email transport, we will utilize Google’s email service.
Now, take a look at the schema of the forgot password flow.
The server will be responsible for sending emails to the user mailbox containing a valid link for password resetting, and will also validate the token and user existence.
Packages and Migration
To begin utilizing the email service and sending emails with Node.js, we need to install the following packages in addition to our existing dependencies:
npm i --save nodemailer handlebars
Nodemailer: Powerful module that allows to send emails easily using SMTP or other transport mechanisms.
Handlebars: Handlebars is a popular templating engine for JavaScript. It will allow us to define templates with placeholders that can be filled with data when rendering.
Now we need to create the migration, so in my case, I have to add a new column forgot_password_token
to users
table:
knex migrate:make add_field_forgot_password_token -x ts
and in the generated file, I set the code:
and then migrate the latest file:
knex migrate:knex
So now we can set to the users
table our forgot_password_token
Routers
To manage controllers responsible for handling the logic of password forgetfulness and resetting, we must establish two routes. The first route initiates the forgot password process, while the second handles the reset process, expecting a token parameter in the URL for verification. To implement this, create a file named forgotPasswordRouter.ts
within the src/routes/
directory and insert the following code:
Two controllers will manage the logic for sending emails and resetting the password.
Forgot Password Controller
When the client forgets his password he has no session, which means we can’t get user data except email or any other security identifiers. In our case, we are sending an email to handle a password reset. That logic we going to set into the controller.
forgotPasswordRouter.post('/', forgotPasswordController);
Remember the ‘forgot password?’ link below the login form usually in UI of any clients in the login form? Clicking on it directs us to a view where we can request a password reset. We simply input our email, and the controller handles all the necessary procedures. Let’s examine the following code:
From the body, we going to get an email, and then we will find the user using UserModel.findByEmail
. If the user exists, we create a JWT token using TokenService.sign
and save the token to the user forgot_password_token
with an expiration of 1 day. Then we will send the message to the email with a proper link together with a token where the user will be able to change his password.
Google Setup
To be able to send email we have to create our new email address which will be a sender.
Let’s go to Google, to create a new email account, and then, when the account is created, proceed to the Manage your Google Account link. You can find it on the top right by clicking on avatar. Then on the left menu click on the Security item and then press 2-Step Verification. Below you will find the App passwords section, click on the arrow:
Input the name that need to use, in my case I set Nodemailer
and press Create.
Copy the generated password and set it to your .env
file. We need to set to file two variables:
MAIL_USER="mygoogleemail@gmail.com"
MAIL_PASSWORD="vyew hzek avty iwst"
Of course, to have a proper email like info@company_name.com
you have to set up Google Workspace, or AWS Amazon WorkMail together with AWS SES, or any other services. But in our case, we are using a simple Gmail account for free.
Email Service
With the .env
file prepared, we are ready to set up our service for sending emails. The controller will provide the service with the generated token and the recipient email address for our message.
await EmailService.sendPasswordResetEmail(email, token);
Let’s define the class for the service:
export class EmailService {}
and now as initial data, I have to get the environment to use with nodemailer
:
We have to take care of service initialization. I was writing about it before in my previous article. Here is an example:
Now, let’s proceed with creating the initialization within our EmailService
class:
There is initialization nodemailer.createTransport()
a method provided by the nodemailer
library. It creates a transporter object that will be used to send our emails. The method accepts an options object as an argument, where you specify the configuration details for the transporter.
We are using Google: service: 'gmail'
specifies the email service provider. Nodemailer provides built-in support for various email service providers, and gmail
indicates that the transporter will be configured to work with Gmail's SMTP server.
For authentication auth
it’s necessary to set the credentials which required to access the email service provider's SMTP server. For user
should be set to the email address from which we are going to send emails, and that password has been generated in Google account from App Passwords.
Now, let’s set the last part of our service:
Before proceeding, it’s crucial to determine the appropriate host for when the client receives an email. Establishing a link with a token in the email body is essential.
For templates, I am using handlebars
and for that, we need to create in src/temlates/passwordResetTemplate.hbs
our first HTML template:
and now we can reuse this template with the helper:
To enhance our email, we can even include attachments. To do so, add the email_logo.png
file to the src/assets
folder. We can then render this image within the email using the following helper function:
After collecting all of those helpers, we have to be able to send email using:
const info = await EmailService.transporter.sendMail({
from: this.env.USER,
to: email,
subject: 'Password Reset',
html: template,
attachments,
});
This approach offers decent scalability, enabling the service to employ various methods for sending emails with diverse content.
Now, let’s try to trigger the controller with our router and send the email. For that, I am using Postman:
The console will tell you that the message has been sent:
Message sent: <1k96ah55-c09t-p9k2–8bv2-j25r9h77f763@gmail.com>
Check new messages in the inbox:
The link to Reset Password has to contain the token and host:
http://localhost:3000/reset-password/<token>
The port 3000
is specified here because this message pertains to the development process. This indicates that the client responsible for handling forms for password reset will also be operating within the development environment.
Reset Password
The token has to be validated on the controller side with TokenService from where we can get the user who sent that email. Let’s recover the router which uses the token:
forgotPasswordRouter.post('/reset/:token', resetPasswordController);
The controller will only update the password if the token is valid and not expired, as per the expiration time set to one hour. To implement this functionality, navigate to the src/controllers/
folder and create a file named resetPasswordController.ts
containing the following code:
This controller will receive the token, verify it, extract the user ID from the decrypted data, retrieve the corresponding user, acquire the new password sent by the client in the request body, and proceed to update the password in the database. Ultimately, this enables the client to log in using the new password.
Conclusion
The scalability of the email service is demonstrated through various approaches, such as sending confirmations or success messages, like those indicating a password update and enabling subsequent login. However, managing passwords is a great challenge, particularly when enhancing application security is imperative. There are numerous options available to bolster security, including additional checks before permitting password changes, such as token comparison, email, and password validation. Another option is to implement a PIN code system, where a code is sent to the user’s email for validation on the server side. Each of these measures necessitates the utilization of email-sending capabilities.
All implemented code you can find in GitHub repository here.
Please feel free to conduct any experiments with this build and share your feedback on what aspects you appreciate about this topic. Thank you so much.
References
Here, you can find several references that I utilized in this article: