Lesson 1
User Authentication with Bcrypt Password
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:

Bash
1npm install bcrypt

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:

TypeScript
1import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 3@Entity() 4export class User { 5 @PrimaryGeneratedColumn() 6 id: number; 7 8 @Column({ unique: true }) 9 username: string; 10 11 @Column() 12 password: string; // This will store the hashed password 13}

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:
TypeScript
1export class CreateUserDto { 2 @IsNotEmpty({ message: 'Username cannot be empty.' }) 3 username: string; 4 5 @MinLength(8, { message: 'Password must be at least 8 characters long.' }) 6 password: string; 7}
  • @Matches(): Ensures that the password contains both letters and numbers:
TypeScript
1export class CreateUserDto { 2 @MinLength(8, { message: 'Password must be at least 8 characters long.' }) 3 @Matches(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/, { 4 message: 'Password must contain both letters and numbers.', 5 }) 6 password: string; 7}
  • @IsNotEmpty(): Ensures fields are not empty:
TypeScript
1export class CreateUserDto { 2 @IsNotEmpty({ message: 'Username cannot be empty.' }) 3 username: string; 4 5 @IsNotEmpty({ message: 'Password cannot be empty.' }) 6 password: string; 7}

To enable validation globally in NestJS, use the ValidationPipe:

TypeScript
1// main.ts 2app.useGlobalPipes(new ValidationPipe());

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

TypeScript
1@Controller('users') 2export class UserController { 3 @Post('register') 4 @UsePipes(new ValidationPipe()) 5 async register(@Body() createUserDto: CreateUserDto) { 6 // Registration logic 7 } 8}

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.

TypeScript
1import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common'; 2import { UserService } from './user.service'; 3import * as bcrypt from 'bcrypt'; 4 5@Controller('users') 6export class UserController { 7 constructor(private readonly userService: UserService) {} 8 9 @Post('register') 10 async register(@Body('username') username: string, @Body('password') password: string, @Res() res) { 11 const hashedPassword = await bcrypt.hash(password, 10); // Hash the password with bcrypt 12 await this.userService.create(username, hashedPassword); // Store the hashed password in the database 13 return res.status(HttpStatus.CREATED).render('index', { message: 'Registration successful!', title: 'Auth App' }); // Send a success message using res.status() 14 } 15}

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:

TypeScript
1import { Injectable } from '@nestjs/common'; 2import { InjectRepository } from '@nestjs/typeorm'; 3import { Repository } from 'typeorm'; 4import { User } from './user.entity'; 5 6@Injectable() 7export class UserService { 8 constructor( 9 @InjectRepository(User) 10 private readonly userRepository: Repository<User>, 11 ) {} 12 13 // Method to create a new user in the database 14 create(username: string, password: string): Promise<User> { 15 const user = this.userRepository.create({ username, password }); 16 return this.userRepository.save(user); 17 } 18 19 // Method to find a user by their username 20 findByUsername(username: string): Promise<User> { 21 return this.userRepository.findOne({ where: { username } }); 22 } 23}

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:

TypeScript
1import { Module } from '@nestjs/common'; 2import { TypeOrmModule } from '@nestjs/typeorm'; 3import { ormConfig } from './typeorm/ormconfig'; 4import { UserModule } from './user/user.module'; 5import { AppController } from './app.controller'; 6import { AppService } from './app.service'; 7 8@Module({ 9 imports: [ 10 TypeOrmModule.forRoot(ormConfig), // Connects to the database 11 UserModule, // Imports the UserModule, which handles user operations 12 ], 13 controllers: [AppController], 14 providers: [AppService], 15}) 16export class 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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.