Authentication is a crucial part of almost every web application. There are many ways to approach it, and we’ve handled it manually in our TypeScript Express series. This time we look into the passport, which is the most popular Node.js authentication library. We also register users and make their passwords secure by hashing. You can find all of the code from this series in this repository. Feel free to give it a star. Defining the User entity The first thing to do when considering authentication is to register our users. To do so, we need to define an entity for our users. users/user.entity.ts import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() class User { @PrimaryGeneratedColumn() public id?: number; @Column({ unique: true }) public email: string; @Column() public name: string; @Column() public password: string; } export default User;The only new thing above is the unique flag. It indicates that there should not be two users with the same email. This functionality is built into PostgreSQL and helps us to keep the consistency of our data. Later, we depend on emails being unique when authenticating. We need to perform a few operations on our users. To do so, let’s create a service. users/users.service.ts import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; import CreateUserDto from './dto/createUser.dto'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository ) {} async getByEmail(email: string) { const user = await this.usersRepository.findOne({ email }); if (user) { return user; } throw new HttpException('User with this email does not exist', HttpStatus.NOT_FOUND); } async create(userData: CreateUserDto) { const newUser = await this.usersRepository.create(userData); await this.usersRepository.save(newUser); return newUser; } } users/dto/createUser.dto.ts export class CreateUserDto { email: string; name: string; password: string; } export default CreateUserDto;All of the above is wrapped using a module. users/users.module.ts import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import User from './user.entity'; @Module({ imports: [TypeOrmModule.forFeature([User])], providers: [UsersService], exports: [UsersService] }) export class UsersModule {} Handling passwords An essential thing about registration is that we don’t want to save passwords in plain text. If at any time our database gets breached, our passwords would have been directly exposed. To make passwords more secure, we hash them. In this process, the hashing algorithm transforms one string into another string. If we change just one character of a string, the outcome is entirely different. The above operation can only be performed one way and can’t be reversed easily. This means that we don’t know the passwords of our users. When the user attempts to log in, we need to perform this operation once again. Then, we compare the outcome with the one saved in the database. Since hashing the same string twice gives the same result, we use salt. It prevents users that have the same password from having the same hash. Salt is a random string added to the original password to achieve a different result every time. Using bcrypt We use the bcrypt hashing algorithm implemented by the bcrypt npm package. It takes care of hashing the strings, comparing plain strings with hashes, and appending salt. Using bcrypt might be an intensive task for the CPU. Fortunately, our bcrypt implementation uses a thread pool that allows it to run in an additional thread. Thanks to that, our application can perform other tasks while generating the hash.npm install @types/bcrypt bcryptWhen we use bcrypt, we define salt rounds. It boils down to being a cost factor and controls the time needed to receive a result. Increasing it by one doubles the time. The bigger the cost factor, the more difficult it is to reverse the hash with brute-forcing. Generally speaking, 10 salt rounds should be fine. The salt used for hashing is a part of the result, so no need to keep it separately.const passwordInPlaintext = '12345678'; const hash = await bcrypt.hash(passwordInPlaintext, 10); const isPasswordMatching = await bcrypt.compare(passwordInPlaintext, hashedPassword); console.log(isPasswordMatching); // true Creating the authentication service With all of the above knowledge, we can start implementing basic registering and logging in functionalities. To do so, we need to define an authentication service first. Authentication means checking the identity of user. It provides an answer to a question: who is the user? Authorization is about access to resources. It answers the question: is user authorized to perform this operation? authentication/authentication.service.ts export class AuthenticationService { constructor( private readonly usersService: UsersService ) {} public async register(registrationData: RegisterDto) { const hashedPassword = await bcrypt.hash(registrationData.password, 10); try { const createdUser = await this.usersService.create({ ...registrationData, password: hashedPassword }); createdUser.password = undefined; return createdUser; } catch (error) { if (error?.code === PostgresErrorCode.UniqueViolation) { throw new HttpException('User with that email already exists', HttpStatus.BAD_REQUEST); } throw new HttpException('Something went wrong', HttpStatus.INTERNAL_SERVER_ERROR); } } // (...) } createdUser.password = undefined is not the cleanest way to not send the password in a response. In the upcoming parts of this series we explore mechanisms that help us with that. A few notable things are happening above. We create a hash and pass it to the usersService.create method along with the rest of the data. We use a try...catch statement here because there is an important case when it might fail. If a user with that email already exists, the usersService.create method throws an error. Since our unique column cases it the error comes from Postgres. To understand the error, we need to look into the PostgreSQL Error Codes documentation page. Since the code for uniqe_violation is 23505, we can create an enum to handle it cleanly. database/postgresErrorCodes.enum.ts enum PostgresErrorCode { UniqueViolation = '23505' } Since in the above service we state explicitly that a user with this email already exists, it might a good idea to implement a mechanism preventing attackers from brute-forcing our API in order to get a list of registered emails The thing left for us to do is to implement the logging in. authentication/authentication.service.ts export class AuthenticationService { constructor( private readonly usersService: UsersService ) {} // (...) public async getAuthenticatedUser(email: string, hashedPassword: string) { try { const user = await this.usersService.getByEmail(email); const isPasswordMatching = await bcrypt.compare( hashedPassword, user.password ); if (!isPasswordMatching) { throw new HttpException('Wrong credentials provided', HttpStatus.BAD_REQUEST); } user.password = undefined; return user; } catch (error) { throw new HttpException('Wrong credentials provided', HttpStatus.BAD_REQUEST); } } }An important thing above is that we return the same error, whether the email or password is wrong. Doing so prevents some attacks that would aim to get a list of emails registered in our database. There is one small thing about the above code that we might want to improve. Within our logIn method, we throw an exception that we then catch locally. It might be considered confusing. Let’s create a separate method to verify the password:public async getAuthenticatedUser(email: string, plainTextPassword: string) { try { const user = await this.usersService.getByEmail(email); await this.verifyPassword(plainTextPassword, user.password); user.password = undefined; return user; } catch (error) { throw new HttpException('Wrong credentials provided', HttpStatus.BAD_REQUEST); } } private async verifyPassword(plainTextPassword: string, hashedPassword: string) { const isPasswordMatching = await bcrypt.compare( plainTextPassword, hashedPassword ); if (!isPasswordMatching) { throw new HttpException('Wrong credentials provided', HttpStatus.BAD_REQUEST); } } Integrating our authentication with Passport In the TypeScript Express series, we’ve handled the whole authentication process manually. NestJS documentation suggests using the Passport library and provides us with the means to do so. Passport gives us an abstraction over the authentication, thus relieving us from some heavy lifting. Also, it is heavily tested in production by many developers. Diving into how to implement the authentication manually without Passport is still a good idea. By doing so, we can get an even better understanding of this process Applications have different approaches to authentication. Passport calls those mechanisms strategies. The first strategy that we want to implement is the passport-local strategy. It is a strategy for authenticating with a username and password.npm install @nestjs/passport passport @types/passport-local passport-local @types/expressTo configure a strategy, we need to provide a set of options specific to a particular strategy. In NestJS, we do it by extending the PassportStrategy class. authentication/local.strategy.ts import { Strategy } from 'passport-local'; import { PassportStrategy }