Search
  • Seiji Ralph Villafranca

Integrating JWT with Nest JS and Mongo

In this content, I will show you how to integrate JSON web token (JWT) authentication in Nest JS, but wait, The main question here is what is Nest JS to be exact? before we proceed to our main topic let me introduce what is Nest JS


What the heck is Nest JS?

Nest JS is basically a progressive Node framework for building server side applications, Rest APIs for example, It is written in TypeScript and it shares many concepts with Angular which is a huge advantage and rejoice for Angular Developers such as using Modules and Services, it was developed by Kamil Myśliwiec and currently it is on v7.6.15.


Some of the key features and advantages of Nest JS as mentioned by Ahmed Bouchefra in his article

https://www.sitepoint.com/introduction-to-nest-js-for-angular-developers/#:~:text=js%3F-,Nest.,Nest.

are the following:


  • Extensibility: Thanks to its modular architecture, Nest allows you to use the other existing libraries in your project.

  • Architecture: Nest has a project’s architecture that provides effortless testability, scalability, and maintainability.

  • Versatility: Nest provides an ecosystem for building all kinds of server-side applications.

  • Progressiveness: Nest makes use of the latest JavaScript features and implements mature solutions and design patterns in software development.

So basically Nest JS is really effective for building Backend APIs and we can say that it is a perfect match for Angular such that few adjustments and concepts are to be made when coding our Backend, to know more about Nest JS you can visit their documentation through this link: https://docs.nestjs.com/


Starting with Nest JS

in this tutorial I assume you already have an existing Nest JS project but if none, we will need to have prerequisite, We need to install Node JS as Nest is ad Node framework and we also need to install Nest CLI.


Prerequisites

After successful installation, we can now scaffold our Nest project by executing the following command:

nest new <project-name>

This will create a new project and configure our Nest JS application with the required dependencies the same with just what Angular CLI is doing. After our application has been successfully configured we will see the files have also been successfully created in our project.




Installing Dependencies

We will now install our needed dependencies for connecting Node JS to MongoDB and integrating JWT, We will be using Passport JWT Library for implementing JSON Web Token and Mongoose for our connection.


Passport and JWT - Dependencies

  • @nestjs/passport

  • @nestjs/jwt

  • passport

  • passpor-jwt

  • passport-local

  • passport-strategy

npm install @nestjs/passport @nestjs/jwt passport passport-local passport-strategy --save

Passport - Dev dependencies

  • @types/passport-jwt

  • @types/passport-local

npm install @types/passport-jwt @types/passport-local --save-dev

MongoDB - Dependencies

  • @nestjs/mongoose

  • mongoose

npm install @nestjs/mongoose mongoose --save

Encryption - Dependencies

  • bcrypt

npm install bcrypt --save

Passport JWT Configuration

User Model Creation

Now we all have our dependencies in place, we will start creating our code, we would need a User Model for us to be able to login, we will also create an endpoint that will allow us to register a new user and retrieve the details of the user if the login is valid.


First we will generate a the Module Controller and Service for our user, we will execute the following commands to achieve this.


Module

nest g module models/user/user --flat

Controller

nest g controller models/user/user --flat

Service

nest g service models/user/user --flat

we can see above that we have place the user inside the models folder such that I will be using the Model folder structure, after generating all three files we should see the following file hierarchy.


Then the next thing we need to do is to create our User schema, we will be using the mongoose extension here for defining decorators. we will create a folder named schema under the user folder and we will create file name user.schema.ts under the schema folder, in the user.schema.ts file we will place the following code.



import { Schema, SchemaFactory, Prop } from "@nestjs/mongoose";
import {Document} from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop( { required: true })
  username: string;
  @Prop({ required: true })
  password: string;

}
export const UserSchema = SchemaFactory.createForClass(User);

We can see above that we have created a schema for our User object, this is identified by the @Schema decorator, to identify the properties of the schema the @Prop decorator is also added. the Document identifies that this is a specific document that will be added in our MongoDB.


We will also create a DTO (Data transfer object) that will be used in receiving our request body, we will create a folder named dto and under this folder we will create a create-user.dto.ts


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

we can see that our dto is just a plain class such that we will only use this for request Body.


Our next step is we need to create our service that would allow us to retrieve a sing le user by username and at the same time will allow us to register a new user, we will now access the functions provided by the Model of mongoose to allow us to insert and query in our database, in our user.service.ts we will place the following code.



import { User, UserDocument } from './schema/user.schema';
import { CreateUserDto } from './dto/create-user.dto';
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { HashService } from 'src/common/services/hash/hash.service';


@Injectable()
export class UserService {

 constructor(@InjectModel(User.name) private userModel: 
 Model<UserDocument>, private hashService: HashService) {}

 async getUserByUsername(username: string) {
   return this.userModel.findOne({ username }).exec();
 }

 async registerUser(createUserDto: CreateUserDto) {
   // validate DTO
   const createUser = new this.userModel(createUserDto);
   
   // check if user exists
   const user = await this.getUserByUsername(createUser.username);
   if(user) {
      throw new BadRequestException();
   }
   
   // Hash Password
   createUser.password = await this.hashService.
   hashPassword(createUser.password); 

    return createUser.save();
 }
} 


In the code above, we have injected our User Model by allowing us to access several functions such as findOne(), find(), save(), deleteOne(), updateOne() and other helpful functions that will allow us to communicate with MongoDB, the getUserByUsername() will return a specific user base on the given username, the registerUser() will allow to create a new user object in our database.


We can see in our code that there is an injected HashService, we will create this service by executing the following command:

npm install common/services/hash --save

and we will place the following code in our hash.service.ts

import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
@Injectable()
export class HashService {

 async hashPassword(password: string) {
   const saltOrRounds = 10;
   return await bcrypt.hash(password, saltOrRounds); 
 }

 async comparePassword(password: string, hash) {
   return await bcrypt.compare(password, hash);
 }
}


the hashPassword() will encrypt the password of the user upon inserting in our database and the comparePassword() will be used to validate if the password in login matches.


Now we have successfully created our module and service for the user, we only need to open endpoints for the register user and get user, in user.controller.ts we will place the following code.



import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('user')
export class UserController {
 
    constructor(private userService: UserService) {}

    @Get(":username")
    getUserByName(@Param() param) {
       return this.userService.getUserByUsername(param.username);
    }
    @Post()
    registerUser(@Body() createUserDto: CreateUserDto) {
       return this.userService.registerUser(createUserDto);
    }
}



and in our user.module.ts we will import MongooseModule and provide our UserService and HashService.


@Module({
 imports: [MongooseModule.forFeature([{ name: User.name, schema: 
           UserSchema }]),
],
 controllers: [UserController],
 providers: [UserService, HashService]
})
export class UserModule {}


this endpoints are now in place and will be working fine but we can see that this has no authentication yet and our getUserByUsername function can be accessed easily, we want to protect our get user endpoint with a JWT, in this case we will create strategies as guards to protect our endpoint as well as we will also create a login endpoint to provide a valid JWT.


Auth Model Creation


we will now generate a the Module Controller and Service for our auth, we will execute the following commands to achieve this.


Module

nest g module models/auth/auth --flat

Controller

nest g controller models/auth/auth --flat

Service

nest g service models/auth/auth --flat

in our auth.service.ts we will place the following code:



import { UserService } from './../user/user.service';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { HashService } from 'src/common/services/hash/hash.service';

@Injectable()
export class AuthService {
 constructor(private userService: UserService, 
 private hashService: HashService,
 private jwtService: JwtService) {}


 async validateUser(username: string, pass: string): Promise<any> {
        const user = await 
        this.userService.getUserByUsername(username);
        if (user && await this.hashService.comparePassword(pass,                      
            user.password)) 
            return user;
        }
            return null;
      }

  async login(user: any) {
        const payload = { username: user.username, sub: user.id };
        return {
          access_token: this.jwtService.sign(payload),
        };
  }
}



the validateUser() function will be used to query a specific username and will check if the user exists and if the password for the given user is valid, this will be used in a strategy later on to protect our login endpoint, the login() function will simply return a valid generated JWT using the user username and user id.


Now we will create an login endpoint with our auth.controller.ts.


import { AuthService } from './auth.service';

import { Controller, Request, UseGuards, Post } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth')
export class AuthController {

    constructor(private authService: AuthService) {}

    @Post("/login")
    async login(@Request() req) {
       return this.authService.login(req.user);
    }
}

we have created our login endpoint but we only want to return the valid JWT if the user and password is valid, this is where the idea of strategies comes in, In this case we will create two strategies, the first one is the local strategy and the second one is the JWT strategy.


Local Strategy

this will protect our login endpoint and check if the username and password in the http request body is valid.


we will create this under the common/strategy folder and name it local.strategy.ts

import { AuthService } from './../../models/auth/auth.service';
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
   constructor(private authService: AuthService) {
      super();
   }
   async validate(username: string, password: string): Promise<any> {
     const user = await this.authService.validateUser(username, 
     password);
     if (!user) {
        throw new UnauthorizedException({message: "You have entered a 
        wrong username or password"});
     }
     return user;
  } 
}

As we can see in the given code above we have used our authentication service to access the validateUser() function to check if the provided username and password is valid. We will now use this as a guard on our login endpoint.

   @UseGuards(AuthGuard('local'))
   @Post("/login")
    async login(@Request() req) {
       return this.authService.login(req.user);
  }


we will now use the @UseGuards decorate and we will pass our local strategy as an AuthGuard Parameter, This means that every request must pass through the guard first and will be authenticated before receiving a valid JWT.


JWT Strategy

now we have created our login endpoint with authentication, let's now protect other endpoints using the generated JWT, We will create a JWT strategy for this instance.


we will create this under the common/strategy folder and name it jwt.strategy.ts


import { jwtConstants } from './../../models/auth/constants';

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';


@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
 constructor() {
 super({
     jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
     ignoreExpiration: false,
     secretOrKey: jwtConstants.secret,
   });
  }

 async validate(payload: any) {
   return { userId: payload.sub, username: payload.username };
 }
}

We have extended the JWT Strategy with PassportStrategyP to extract the JWT from the request headers, This will only return the user id and username if the JWT is valid (you should create a secret key and place this on a constants.ts file).


We want to protect our get user endpoint in user.controller.ts, we will achieve this by using the @UseGuards decorator as we have down in our login endpoint.

   @UseGuards(AuthGuard('jwt'))
   @Get(":username")
    getUserByUsername(@Param() param) {
       return this.userService.getUserByUsername(param.username);
    }

This endpoint would now only return the user if the request has a valid JWT in the request header.


Don't forget to update our auth.module.ts and user.module.ts as we have added strategies to our application.


auth.module.ts

@Module({
 imports: [MongooseModule.forFeature([{ name: User.name, schema:            
           UserSchema }]), 
           PassportModule,
           JwtModule.register({
                  secret: jwtConstants.secret,
                  signOptions: { expiresIn: '24h' },
           }),
 ],
 providers: [AuthService, UserService, HashService, LocalStrategy],
 controllers: [AuthController  ]
})
export class AuthModule {}

user.module.ts

@Module({
 imports: [MongooseModule.forFeature([{ name: User.name, schema:  
          UserSchema }]),
          JwtModule.register({
                  secret: jwtConstants.secret,
                  signOptions: { expiresIn: '24h' },
          }),
 ],
 controllers: [UserController],
 providers: [UserService, HashService, AuthService, JwtStrategy, LocalStrategy]
})
export class UserModule {}


we have imported our User schema using the MongooseModule to be identified what are the existing schemas that are to be created in our MongoDB, we have also registered our secret key in JWT Module,.


and now we will establish the connection to our MongoDB in our our root module which is the app.module.ts and also import our two modules AuthModule and UserModule



@Module({
 imports: [MongooseModule.forRoot('mongodb+srv://admin: 
   <password>@cluster0.l1zps.mongo 
   db.net/<dbname>?retryWrites=true&w=majority'),
 AuthModule,
 UserModule,
 
],
 controllers: [AppController],
 providers: [AppService, HashService],
})
export class AppModule {}

the connection string Mongo DB will be used for our connection to identify which cluster and database we will connect. (replace this string with your connection string with the user, password and database name).


and that's it our Nest JS is configured with Passport JWT authentication, we can run the command npm run start:dev to start our local node server, on a successful start we can test our created endpoints.



POST /user - to create a user

we have used our /user endpoint to create a valid user and we can now used this credential for us to generate a valid token.


POST auth/login - to generate a valid JWT

we can now see that our endpoint has a response of a valid JWT such that our credentials is now valid, we can use this jwt to access our get user endpoint.


GET /user - to get the specifc

By placing our token in our header with a key of Authorization, we have successfully retrieved the user (remember not to include the password as this is only for demonstration purposes).


And our Nest JS with Passport is complete! I have only configured here a working JWT and Passport Strategy for Nest JS, some validations such as DTO and password strength validations is not applied, this is another topic you can explore for more secure endpoints on our Backend.


I hope you learned a lot on this content, for the whole example code I have place it here in my Github repository https://github.com/SeijiV13/nest-jwt, do follow me on Github for more example codes and tutorials Cheers!

46 views0 comments

Recent Posts

See All