pwshub.com

How to Add JWT-Based Authentication in NestJS

How to Add JWT-Based Authentication in NestJS

Authentication is a very important aspect of software development. It is the process of verifying a user’s identity.

Authentication ensures that only authorized individuals access specific resources or perform certain actions within a system. It provides accountability by enabling the tracking of user actions and holding individuals responsible for their activities.

It also gives companies data on the number of people using their products. Without proper authentication of your software, you may face security risks. Proper implementation prevents unauthorized access and protects sensitive data.

This tutorial will guide you through building a JWT-based user authentication in NestJS and MongoDb.

NestJS is a powerful Node.js framework for building server-side
applications. MongoDB is popular NoSQL database and we’ll use it to build the basic authentication endpoints.

In this tutorial, we'll cover the following topics:

  • How to creating a new NestJS project and install the necessary dependencies.
  • How to create user models and schemas.
  • How to implementing login and signup with JWT token.
  • How to test the endpoints with postman.

Prerequisites

This tutorial is a hands-on demo. To follow along, you must have the
following:

  • Node.js v14
    and above

  • Node package manager

  • MongoDB
    compass

  • Basic understanding of Node.js and preferably ExpressJs

  • A code editor (for example: VS Code)

How to Set Up the Project.

In this section, we’ll set up the project to build our REST API with
NestJS and MongoDB. We’ll start by installing the NestJS CLI and use it to generate new projects.

Install Nest CLI:

$ npm i -g @nestjs/cli

Then create a new NestJS project with this command:

$ nest new project-name

Let’s call the project authentication:

$ nest new authentication

You will see options about which package manager you prefer to install. I
used npm.

After successful installation, move into the created directory and open
the project with your preferred code editor.

Before we go into creating resources and configuring the project, let’s
take a quick look at the src directory and its files:

  • src: The root directory for the source code.

  • src/app.module.ts: The application's main module for configuring and
    integrating other modules.

  • src/app.controller.ts: Contains a default controller with a single
    route.

  • src/app.service.ts: This includes a basic service using a single
    method.

  • src/main.ts: The entry point of the application.

Next, install dependencies to set up and connect the database. You will have to install the mongoose package, bcrypt:

$ npm i -save @nestjs/mongoose @types/bcrypt mongoose bcrypt

dependencies succefully installed

Next, set up your database. The MongooseModule from the @nestjs/mongoose dependency will be used to set up the database.

Go to src/app.module.ts file:

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/auth-workshop'),
UsersModule,
AuthModule,
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Let's breakdown the code above.
The app.module.ts is the root module of the nestjs application and it is responsible for importing dependecies and other modules required by the application, configuring the application such as database connections and environment variables.

We first specified the app.module.ts file as a module by using the @Module({}) decorator:

@Module({})

The imports array specifies the module that this module depends on. We have the MongooseModule.forRoot('') for database connection using mongoose, ConfigModule.forRoot import sets up the configuration module to read from a .env file:

import { MongooseModule } from '@nestjs/mongoose';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/auth-workshop'),
UsersModule,
AuthModule,
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
]

You can replace the MongooseModule.forRoot() URI string with your own database
string.

The controllers array specifies the controller that belongs to this module:

controllers: [AppController]

The providers array specifies the services that belongs to this module:

providers: [AppService]

With these configurations, you can start your application by using the
npm run start:dev.

User models and schemas

In this section, we'll define the User model and schema using
mongoose. The User model will represent the users of the application,
and the schema will define the structure of the data stored in MongoDB.
Let’s start by creating a resource module for the users API:

First, create a users resource with the nest CLI:

$ nest generate res users

This command will create a new users resource in the src/users
directory with a basic structure of controllers and services.

Then define the schema for the Users in src/users/entities/users.entity.ts:

import { Prop, Schema, SchemaFactory }
from '@nestjs/mongoose';
@Schema()
export class User {
@Prop()
name: string;
@Prop({ unique: true })
email: string;
@Prop()
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);

The @Schema decorator is what makes the class a schema. This schema defines three fields for the users: name, email, and password. They are all typed string.

Next, register the schema in the users.module.ts file located in src/users/users.module.ts:

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema, User } from './entities/user.entity';
@Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema:
UserSchema }])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}

By importing the MongooseModule.forFeature([{}]), this configures mongoose to use the UserSchema for the User model.

By registering the schema with Mongoose, we created a model that's ready to use in our application. This sets the stage for building services and controllers that can interact with our data and handle incoming requests.

Lastly, define the DTO (data transfer object). This object transfers data between systems. It is a simple object that contains only data and has no behaviour.

Create your CreateUserDto in the src/users/dto/create-user.dto.ts directory:

export class CreateUserDto {
username: string;
email: string;
password: string;
}

User Services and Controllers

We have connected to the database and created the schema and model of Users. Let’s write the basic CRUD methods for the users.

Update your users.service.ts located in src/users/users.service.ts:

import { Injectable, NotFoundException
} from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectModel } from '@nestjs/mongoose';
import { User } from './entities/user.entity';
import { Model } from 'mongoose';
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel:
Model<User>) {}
async createUsers(createUserDto: CreateUserDto) {
const user = await this.userModel.create(createUserDto);
return user.save();
}
async findAllUsers() {
const users = this.userModel.find();
return users;
}
async findUser(id: number) {
const user = await this.userModel.findById(id);
if (!user) throw new NotFoundException('could not find the user');
return user;
}
updateUser(id: number, updateUserDto: UpdateUserDto) {
return this.userModel.findByIdAndUpdate(id, updateUserDto, { new: true
});
}
removeUser(id: number) {
return this.userModel.findByIdAndDelete(id);
};
}

Update your users.controller.ts located in src/users/users.controller.ts:

import { Controller, Get, Post, Body, Patch, Param, Delete, } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return await this.usersService.createUsers(createUserDto);
}
@Get()
async findAll() {
return await this.usersService.findAllUsers();
}
@Get(':id')
async findOne(@Param('id') id: string) {
return await this.usersService.findOneUser(+id);
}
@Patch(':id')
async update(@Param('id') id: string, @Body() updateUserDto:
UpdateUserDto) {
return await this.usersService.updateUser(+id, updateUserDto);
}
@Delete(':id')
async remove(@Param('id') id: string) {
return await this.usersService.removeUser(+id);
}
}

We created RESTful APIs using mongoose. The methods include:

  • findAllUsers: Retrieves all user documents from the collection.

  • getUserById: Finds a single user document by ID.

  • createUsers: Adds a new user document to the collection.

  • updateUser: Updates details of existing user in the collection.

  • removeUser: Removes user document by ID.

How to Implement Sign-up and Log-in

In this section, we'll create an authentication service that generates JSON Web Tokens (JWTs).

This service will have two methods: signup and login. The signup
method will take a signup request object containing name, email, and
password),and the login method will take a login request object
containing email and password. Both methods will return a JWT.

Let’s start by creating a resource module for the authentication APIs.

Create auth resource with Nest CLI:

$ nest generate res auth

Then define the DTO for both signup and login. Go to the dto folder in the src/auth/dto folder:

signup.dto.ts:

export class SignUpDto {
name: string;
email: string;
password: string;
}

login.dto.ts:

export class Login {
email: string;
password: string;
}

Next, create a .env file in the root directory:

JWT_SECRET=secret
JWT_EXPIRES=3d

Then add the PassportModule, JwtModule and JwtStrategy to AuthModule. We'll start by installing these packages:

$ npm i @nestjs/passport @nestjs/jwt passport passport-jwt bcryptjs

Go to src/auth/auth.module.ts and import these packages:

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from 'src/users/entities/user.entity';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return {
secret: config.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: config.get<string | number>('JWT_EXPIRES'),
},
};
},
}),
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}

The PassportModule.register configures the Passport module to use the JWT strategy as the authentication mechanism.

The JwtModule.registerAsync configures the JWT module using asynchronous registration process, such a the token expiration time.

The MongooseModule.forFeature() configures mongoose to us the UserSchema for the User model

These imports enable the authentication module to manage user authentication, JWT generation, and database interaction.

Next, create the AuthServices for signup and login in the src/auth/auth.service.ts directory:

import { Injectable, UnauthorizedException} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from 'src/users/entities/user.entity';
import { SignUpDto } from './dto/signup.dto';
import * as bcrypt from 'bcryptjs';
import { LoginDto } from './dto/login.dto';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
@InjectModel(User.name) private userModel: Model<User>,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async signUp(signupDto: SignUpDto) {
const { name, email, password } = signupDto;
const hashedPassword = await bcrypt.hash(password, 10);
const user = await this.userModel.create({
name,
email,
password: hashedPassword,
});
await user.save();
const token = await this.jwtService.sign(
{ id: user.id },
{
secret: this.configService.get('JWT_SECRET'),
expiresIn: this.configService.get('JWT_EXPIRES'),
},
);
return { token };
}
async login(loginDto: LoginDto) {
const { email, password } = loginDto;
const user = await this.userModel.findOne({
email,
});
if (!user) throw new UnauthorizedException('invalid email or
password');
const passwordMatch = await bcrypt.compare(password,
user.password);
if (!passwordMatch)
throw new UnauthorizedException('invalid email or password');
const token = await this.jwtService.sign(
{ id: user.id },
{
secret: this.configService.get('JWT_SECRET'),
},
);
return { token };
}
}

In the AuthService, the signUp method facilitates user registration by:

  • Destructuring the SignupDto object to extract user credentials.
  • Hashing the password using bcrypt for secure storage.
  • Creating a new user document in the database via the userModel.
  • Saving the user document to the database.
  • Generating a JWT token using jwtService upon successful registration.
  • Returning the JWT token to the client.

The login method authenticates users by:

  • Destructuring the LoginDto object to verify user credentials.
  • Comparing the input password with the stored hash to ensure a match.
  • Throwing an UnauthorizedException error if the passwords do not match.
  • Utilizing the jwtService to sign the user and generate a JWT token upon successful authentication.
  • Returning the JWT token to the client.

Update the AuthController for signup and login in the src/auth/auth.controller.ts directory:

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignUpDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('signup')
signUp(@Body() signupDto: SignUpDto) {
return this.authService.signUp(signupDto);
}
@Post('login')
signin(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
}

With these steps, you’ve implemented a basic user login and signup in your application. In the next sections, we’ll test the login and signup routes.

Testing in Postman

Now that we've set up our endpoints, it's time to put them to the test.
For this example, I'll be using Postman as my API client, but feel free
to use any tool or client that suits your needs. Let's see our API in
action!

How to Create a User With the /signup Endpoint:

postman signup test

After sending a POST request to the /signup endpoint using Postman, we
received a response containing the accessToken. You can verify that a new
user has been successfully created by checking the database.

mongodb compass signup verification

Login as an existing user with the /login endpoint:

postman signin test

Conclusion

Congratulations, you've successfully implemented comprehensive
authentication using NestJS, Mongoose, and Passport. We designed a secure
signup and login process, and generated JSON Web Tokens (JWTs).

To further improve and expand your knowledge on authentication, look
into authorization, protecting routes with authentication middleware,
and implementing email verification and password reset functionality.

This foundation provides a solid starting point for building a robust
and scalable authentication system. This project was a pleasure to work
on, and I hope you found it equally enjoyable.

For your convenience, the project repository is available on Github.

Please don't hesitate to connect with me on Twitter at @Adedot1Abimbola.
I'd love to hear from you

Source: freecodecamp.org

Related stories
6 days ago - Auth.js makes adding authentication to web apps easier and more secure. Let's discuss why you should use it in your projects. The post Auth.js adoption guide: Overview, examples, and alternatives appeared first on LogRocket Blog.
1 month ago - In this adoption guide, we’ll discuss some reasons to choose Fastify, key features, compare Fastify to some popular alternatives, and more. The post Fastify adoption guide: Overview, examples, and alternatives appeared first on LogRocket...
1 month ago - In this in-depth guide, I’ll be showing how to secure a Next.js AI app deployed on Vercel. We’ll be taking a hands-on approach by starting with a simple AI app riddled with vulnerabilities. This article will guide you through how you can...
1 month ago - There are different ways to implement an authorization system and the one you chose depends on your application's needs. Attribute-Based Access Control (ABAC) is just one of them, so let's go ahead and learn how to implement it in a Rails...
1 minute ago - Learn how to set up an Amazon API Gateway and secure the REST API with Okta Customer Identity Cloud (CIC) Adaptive MFA Risk Score
Other stories
1 hour ago - This release candidate, a near-final look at Deno 2, includes the addition of Node's process global, better dependency management, and various API stabilizations, and more.
1 hour ago - Published: September 19, 2024 The CSS Working Group has combined the two CSS masonry proposals into one draft specification. The group hopes that...
1 hour ago - Stay organized with collections Save and categorize content based on your preferences. Published: September...
3 hours ago - DNS monitoring tool is a cloud-based scanner that constantly monitors DNS records and servers for anomalies. This tool aims to ensure that users are sent to genuine and intended website pages, instead of fakes or replicas. It alerts users...
4 hours ago - Email spoofing is a malicious tactic in which cybercriminals send fake emails that look like they come from trusted organizations or individuals. When unsuspecting users act on these emails, they may unknowingly share sensitive data,...