Introduction to User Authentication with Bcrypt

Welcome to "Securing and Testing your MVC NestJS App"! In this lesson, we’ll focus on user authentication using bcrypt. This is an essential step in securing your application by ensuring that user passwords are stored safely.

We'll also be using bcrypt to hash passwords as part of our authentication mechanism in the next section.

What You'll Learn

By the end of this lesson, you will learn how to:

  • Create a User entity in NestJS.
  • Hash passwords using bcrypt.
  • Register users with secure password storage.
  • Implement a basic user registration mechanism
User Authentication with Bcrypt

When building applications that require user authentication, it is critical to store passwords securely. Bcrypt is a widely-used library that helps hash passwords, making it difficult for attackers to retrieve the original passwords even if they gain access to your database.

Before we begin, make sure you install the bcrypt package:

This setup is already done in the Practice Setup, so you don't need to repeat it.

The User Entity

We start by defining a User entity to represent users in our application database. This entity will contain the username and the hashed password:

The User entity contains three main fields: id, username, and password. We use the { unique: true } option for the username column to ensure that each username in the database is unique. The password field will store the hashed password after we apply bcrypt.

Adding Validation Techniques

To enhance the security of our application and ensure only valid data is processed, we can add validation constraints directly in a DTO (Data Transfer Object) using NestJS's class-validator decorators. The DTO handles incoming data validation before it's passed to business logic.

Here are some useful validation techniques:

  • @MinLength(): Enforces a minimum length. For example, to ensure the password is at least 8 characters long:
  • @Matches(): Ensures that the password contains both letters and numbers:
  • @IsNotEmpty(): Ensures fields are not empty:

To enable validation globally in NestJS, use the ValidationPipe:

You can also apply it to specific methods in a controller:

By applying the ValidationPipe, NestJS ensures all validation rules in your DTO are enforced globally or at the method level.

With these validation techniques applied in the DTO, we ensure that the password meets specific security standards, like requiring a certain length and containing both letters and numbers, before being passed to the service for processing. This validation mechanism helps safeguard your application by preventing invalid data from being persisted in the database.

Creating Users with Hashed Passwords

Next, we 'll create the controller that will handle user registration. This controller will take the user’s password, hash it using bcrypt, and save the hashed password to the database. However, let's first see why hashing passwords is important.

Why Hashing Passwords Is Important

Storing plain text passwords in a database is a bad practice because anyone with access to the database could easily see users’ passwords. By hashing the password using bcrypt, we ensure that even if an attacker gains access to the database, they can't read the actual passwords.

The 10 in bcrypt.hash(password, 10) refers to the cost factor, which controls how computationally expensive it is to generate a password hash. The number 10 means the hashing algorithm will run 2^10 rounds (or 1024 iterations). The higher the number, the longer it takes to hash the password, increasing security by making brute-force attacks more difficult. However, higher rounds also make the login process slower, so balancing security and performance is important.

In the register method:

  • We take the user's plain text password.
  • Bcrypt is used to hash the password with a salt factor of 10, making it computationally expensive for attackers to reverse-engineer.
  • After hashing, the user’s data is stored in the database using the UserService.
  • We use res.status(HttpStatus.CREATED).render(...) to send a success message along with the HTTP 201 status code, indicating successful creation.

The 'index' in res.render('index', {...}) refers to the name of the Handlebars template (index.hbs) that will be rendered when this route is accessed. This method of rendering allows us to dynamically pass variables to the template, like { message: 'Registration successful!' }. By using res.status() combined with res.render(), you have full control over both the HTTP status and the content of the response. This approach is preferred over the @Render() decorator because it gives you flexibility to manage different HTTP status codes, which is essential for authentication scenarios (e.g., success vs. error handling).

Commonly Used HTTP Statuses

When handling responses, it’s important to send the correct HTTP status code using res.status(). Here are a few common ones that you'll use in your application:

  • 201 (CREATED): Used when a new resource is successfully created. In the register method, we use res.status(HttpStatus.CREATED) to indicate that the user has been successfully registered.
  • 200 (OK): This status is used for successful responses where no new resources were created. For example, after a successful login, you might use res.status(HttpStatus.OK).
  • 401 (UNAUTHORIZED): Used when the user is not authorized to access the resource. For failed login attempts, we use res.status(HttpStatus.UNAUTHORIZED) to inform the client that the credentials are invalid.
  • 400 (BAD REQUEST): Used when the client sends invalid data. You might use this status code when the client sends malformed or missing data in the registration or login form.

Using res.status() allows you to explicitly define how your server responds to different situations, giving you full control over the response sent to the client.

Storing Users in the Database

The UserService is responsible for interacting with the database to store and retrieve users. Here’s how we handle that:

In this service:

  • The create method generates a new user object and stores it in the database. The password stored here is already hashed.
  • The findByUsername method allows us to look up a user by their username, which will be useful when we implement user login and authentication in the next section.
Putting It All Together

To finalize, we connect everything in our AppModule:

Here:

  • The TypeOrmModule is configured to connect to our database.
  • The UserModule is imported to manage user-related functionality.

This setup provides a robust foundation for securely handling user data using bcrypt, and we'll expand on this by implementing user authentication in the next section.

Why It Matters

Securing user passwords is essential for:

  • Protecting User Data: Password hashing ensures that user data is safe, even if the database is compromised.
  • Building Trust: By implementing secure password storage, users are more likely to trust your application.
  • Compliance: Storing passwords in plain text is a security risk and often against best practices or legal standards in many industries.

Now that you’ve learned how to hash and store passwords securely, it’s time to move on to the practice section and start implementing this in your NestJS application!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal