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.
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
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:
Bash1npm install bcrypt
This setup is already done in the Practice Setup, so you don't need to repeat it.
We start by defining a User
entity to represent users in our application database. This entity will contain the username
and the hashed password
:
TypeScript1import { 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.
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:
TypeScript1export 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:
TypeScript1export 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:
TypeScript1export 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
:
TypeScript1// main.ts 2app.useGlobalPipes(new ValidationPipe());
You can also apply it to specific methods in a controller:
TypeScript1@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.
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.
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.
TypeScript1import { 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).
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 useres.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.
The UserService
is responsible for interacting with the database to store and retrieve users. Here’s how we handle that:
TypeScript1import { 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. Thepassword
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.
To finalize, we connect everything in our AppModule
:
TypeScript1import { 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.
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!