DEV Community

Felix Owino
Felix Owino

Posted on • Updated on

Hashing and Verifying Passwords in Mongoose Schema

Are you creating a web application that is going to require users to sign in with passwords? It's a good security measure to store passwords in the database as hashed strings instead of plain strings. Hashing passwords is done to protect users' passwords. In case an attacker gains access to your database, they will see the hashed passwords but they won't be able to use them.

In this article, we will learn how to create a user schema that hashes users' passwords before saving them to the database. We will also learn how to add an instance method to the schema that verifies user passwords during login.

We will not cover other surrounding topics such as user authentication and user registration middleware in this article. The main goal of this article is to show you how to create a self-contained schema. This schema will make it easy for you to implement user registration and authentication middleware without worrying about password hashing.

We will use Typescript in this article. This article does not go into depth about how to create models and schema with Mongoose and Typescript. Therefore, this article assumes that you know how to create Mongoose schema using Typescript. If you are new to using Typescript, visit this article on creating Mongoose schema with Typescript before continuing.

Before we dive into coding, let us list out the procedure we are going to follow.

  1. Initialize a working directory and configure Typescript.
  2. Create the user schema.
  3. Implement password hashing.
  4. Implement password Verification.

Let us start by creating a working directory.

1. Initializing working directory and configure Typescript.

In this step, we will clone a pre-configured working directory from GitHub. You can also create your own directory with configurations of your choice. If you need help, see how to set up an initial project directory for Node and Typescript.

Your working directory should have something looking like this.

Initial working directory

After successfully cloning the repository, install its dependencies using the following command:

npm install
Enter fullscreen mode Exit fullscreen mode

With the working directory set up, let us create the user schema.

2. Creating the user schema.

To create a Mongoose schema, we need to install mongoose and import it to our models file.

Install mongoose using the following command:

npm install mongoose
Enter fullscreen mode Exit fullscreen mode

Create the User.model.ts file in the src directory of your project and import Mongoose.

import mongoose from "mongoose"
Enter fullscreen mode Exit fullscreen mode

Create an interface for the raw document as created in the code snippet below.

interface IUser{
    first_name: string,
    last_name: string,
    password: string,
    username: string
}
Enter fullscreen mode Exit fullscreen mode

Create the user schema that implements the interface as created in the code snippet below.

const userSchema = new mongoose.Schema<IUser>({
    first_name: String,
    last_name: String,
    password: String,
    username: String
})
Enter fullscreen mode Exit fullscreen mode

Now we have successfully created a user schema, let's add password hashing logic in step 3.

3. Implement password hashing.

In the last step, we implemented a simple user schema. Here, we will add a functionality that will hash every password entered by the user before saving.

To add a logic that is executed before saving a document, we use the pre hook of the Mongoose Schema.

We will use the bcrypt module to hash user passwords. Install the bcrypt module and its types using the following commands.

npm install bcrypt
npm install -D @types/bcrypt
Enter fullscreen mode Exit fullscreen mode

Import the hash function from bcrypt as follows.

import { hash } from 'bcrypt'
Enter fullscreen mode Exit fullscreen mode

The following code snippet shows how the password hashing on save can be implemented on the schema.


userSchema.pre('save', async function(next){

    const hashedPassword =  await hash(this.password, 10)
    this.password = hashedPassword

    next()
})
Enter fullscreen mode Exit fullscreen mode

The above code snippet does the following:

  • Convert the raw password into a hash string.
  • Replace the original raw password with the hashed password.
  • Call the next callback function. The callback signals Mongoose that we are done hashing the password and it can now be saved to the database.

It is important not to assume the pre save hook to work across every database operation. The pre save hook only encrypts the password on the initial creation. The pre save hook is not invoked by any other database operation that does not call the save method. This means that an update operation will not invoke the password encryption function at all. If you want to change your password, you have to encrypt it in a separate program otherwise it will be stored in the database as plain text.

At this point, we are done with the password hashing functionality. Let us add an instance method to verify passwords in the next step.

4. Implement password verification.

In the previous section, we added password hashing to our schema. In this section, we will add a password verification method to the user schema.

Before adding the method to the schema, we need to create an interface for instance methods. The following code snippet shows the declaration of the interface with the signature for the verification function.

interface UserMethods{
    isValidPassword:(password: string) => Promise<boolean>
}
Enter fullscreen mode Exit fullscreen mode

Next, we need to add the interface to the Schema constructor and Model type as follows.

type UserModel = Model<IUser,{}, UserMethods>

const userSchema = new mongoose.Schema<IUser, UserModel, UserMethods>({
    first_name: String,
    last_name: String,
    password: String,
    username: String
})
Enter fullscreen mode Exit fullscreen mode

Finally, we implement the isValidPassword method as follows:

Import the compare function and implement the method.

import { compare } from 'bcrypt'
Enter fullscreen mode Exit fullscreen mode
userSchema.method('isValidPassword', async function(
    password: string): Promise<boolean>{
    const isValid = await compare(password, this.password)
    return isValid
})
Enter fullscreen mode Exit fullscreen mode

The isValidPassword method will be accessible through the user document. In the implementation of authentication, the method isValidPassword can be called on the user document. The method returns a promise. The promise resolves to true or false depending on whether the user entered the correct password or not.

Export the model as follows.

export const User = model<IUser, UserModel>('User', userSchema)
Enter fullscreen mode Exit fullscreen mode

Conclusion

Finally, we have learned how to create a self-contained user schema. The schema can convert a password into a hash string before saving it to the database. The schema can also verify user passwords against the stored hashed passwords during authentication. Visit this article to learn more about creating Mongoose models with Typescript.

Top comments (2)

Collapse
 
sanskari_patrick07 profile image
Prateik Lohani • Edited

Would your hash password middleware not be called multiple times and change the password every time? Even if i change just the name in the data field, my password will get encrypted again because I'm saving the data.

What do you think?

Collapse
 
ghostaram profile image
Felix Owino

You might think so but that's not the case. The pre save hook is only called when the document is created and saved by calling the save function. By default, the save function is only called on initial creation of the document. The pre save hook is not called during updates even if the password is also being updated. If you fail to encrypt your password during update, it will be stored as plain text in the database. It's not a shortcut at all.

See more from Mongoose docs mongoosejs.com/docs/middleware.htm...