Lesson 1
Creating Users with Encrypted Passwords via Bcrypt in NestJS
Introduction and Context

Welcome to the first lesson on authenticating users in your ToDo app with NestJS! In this course, we aim to guide you through setting up secure user authentication using NestJS, a powerful Node.js framework. In this lesson, we'll focus on password security and how to create users with encrypted passwords.

Password security is crucial in web applications to protect user data from unauthorized access. Encryption ensures that even if passwords are exposed, they remain unreadable to attackers. We'll be using bcrypt, a popular library for secure password hashing, which adds random salt to each password, making it difficult for attackers to use common techniques to crack the passwords.

Understanding Bcrypt

To begin, let's dive into bcrypt's role in securing passwords with the concept of one-way hashing. One-way hashing is a fundamental security measure that transforms any input, such as a password, into a fixed-size string of random characters, known as a hash. This transformation is non-reversible, meaning that it is computationally infeasible to convert the hash back to its original input. This property is crucial for protecting passwords, as it ensures that even if a hash is compromised, the actual password cannot be easily retrieved by an attacker.

Incorporating one-way hashing with bcrypt into our user authentication system in NestJS strengthens password security by rendering the stored password data useless to attackers, even if they gain access to the User database.

While bcrypt is an excellent choice for our application, alternatives exist such as Argon2 and PBKDF2, each with unique features and security levels. However, bcrypt remains popular due to its balance of security and performance.

User Schema Setup in NestJS

The first step in creating an encrypted user registration system is defining the user schema for MongoDB. In NestJS, we use mongoose to define our data model. Here's how we set up a basic USER schema:

TypeScript
1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2import { HydratedDocument } from 'mongoose'; 3 4export type UserDocument = HydratedDocument<User>; 5 6@Schema() 7export class User { 8 @Prop({ required: true }) 9 username: string; 10 11 @Prop({ required: true }) 12 password: string; // This will store the hashed password 13} 14 15export const UserSchema = SchemaFactory.createForClass(User);

In this example, the user schema has two required properties: username and password. When creating a user, we will ensure the password is stored in an encrypted form. The SchemaFactory.createForClass method generates the mongoose schema based on the User class.

Implementing User Registration with Bcrypt

Next, let's implement user registration while hashing passwords using bcrypt. We'll add this functionality in the AuthService:

TypeScript
1import { Injectable } from '@nestjs/common'; 2import * as bcrypt from 'bcrypt'; 3import { UserService } from '../user/user.service'; 4 5const SALT_ROUNDS = 10; 6 7@Injectable() 8export class AuthService { 9 constructor(private usersService: UserService) {} 10 11 async register(username: string, unhashedPassword: string): Promise<string> { 12 const password = await bcrypt.hash(unhashedPassword, SALT_ROUNDS); 13 const user = await this.usersService.create(username, password); 14 return user.username; 15 } 16}

In this code, we're using bcrypt to hash the password before storing it in the database. The bcrypt.hash() function takes the unhashedPassword and transforms it into a secure hash using ten salt rounds. After creating the user, we clear out the password from the returned object for additional security.

What is Salt?

The SALT_ROUNDS (10 in our case) in bcrypt.hash(unhashedPassword, SALT_ROUNDS) refers to the cost factor, which controls how computationally expensive it is to generate a password hash. In this case, 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 the security by making brute-force attacks more difficult. However, higher rounds also make the login process slower, so a balance between security and performance is important.

Example Code Detailed Walkthrough

Let's walk through the full example code to see how these elements come together:

User Service

The UserService handles user creation and retrieval but is not concerned with authorization details. We left that to the Auth:

TypeScript
1import { Injectable } from '@nestjs/common'; 2import { InjectModel } from '@nestjs/mongoose'; 3import { Model } from 'mongoose'; 4import { User, UserDocument } from './schemas/user.schema'; 5 6@Injectable() 7export class UserService { 8 constructor(@InjectModel(User.name) private readonly userModel: Model<UserDocument>) {} 9 10 async create(username: string, hashedPassword: string): Promise<UserDocument> { 11 const createdUser = new this.userModel({ username, password: hashedPassword }); 12 return await createdUser.save(); 13 } 14 15 async findByUsername(username: string): Promise<UserDocument | null> { 16 return await this.userModel.findOne({ username }).exec(); 17 } 18 19 // TEMPORARY FOR TESTING 20 async findAll() { 21 return await this.userModel.find().exec(); 22 } 23}

The UserService defines methods for creating and retrieving users. The create method saves a new user with a hashed password. Notice the use of the create method within mongoose to save the new user document.

Auth Controller

The AuthController exposes an endpoint for user registration:

TypeScript
1import { Controller, Post, Body } from '@nestjs/common'; 2import { AuthService } from './auth.service'; 3import { UsernameWithPassword } from './dtos/auth.dto'; 4 5@Controller('auth') 6export class AuthController { 7 constructor(private readonly authService: AuthService) {} 8 9 @Post('register') 10 async register(@Body() createUserDto: UsernameWithPassword) { 11 const { username, password } = createUserDto; 12 return await this.authService.register(username, password); 13 } 14}

In the AuthController, we declare the /register endpoint, which accepts the UsernameWithPassword DTO containing the user input for registration. The register method in AuthService is then called to create a new user with the encrypted password.

Summary and Practice Preparation

In this lesson, we covered the importance of encrypting passwords to protect user information and how to use bcrypt to securely manage passwords in our NestJS application. We implemented a user schema, created a registration process with hashed passwords, and walked through how these components interact within the application.

As we conclude, you're encouraged to get hands-on with the upcoming practice exercises to solidify your understanding and further explore user authentication in NestJS. You'll have the opportunity to create users, verify the encryption process, and interact with the application code. This foundational knowledge will be critical as we continue developing the ToDo app with added functionalities.

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