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.
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.
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:
TypeScript1import { 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.
Next, let's implement user registration while hashing passwords using bcrypt
. We'll add this functionality in the AuthService
:
TypeScript1import { 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.
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.
Let's walk through the full example code to see how these elements come together:
The UserService
handles user creation and retrieval but is not concerned with authorization details. We left that to the Auth
:
TypeScript1import { 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.
The AuthController
exposes an endpoint for user registration:
TypeScript1import { 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.
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.