JULY 31, 2024 / #AUTHENTICATION
How to Add JWT-Based
Authentication in NestJS
Abimbola Adedotun Samuel
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
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
});
}
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, } fro
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:
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.
Login as an existing user with the /login
endpoint:
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