NestJS Authentication JWT MongoDB - Do It Right
NestJS Authentication JWT MongoDB - Do It Right

In this post we will implement together NestJS authentication. Which actually means we will implement registration, loginning, obtaining JWT token and authorizing our requests.

I already generated for us an empty NestJS application with prepared files for our feature.

// src/user/user.module.ts
...
import {UserController} from './user.controller';
import {UserService} from './user.service';
@Module({
  imports: [],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

Here we just registered our UserController and UserService.

// src/user/user.controller.ts
@Controller()
export class UserController {
  constructor(private readonly userService: UserService) {}
}

It is completely empty and we just injected our UserService inside.

// src/user/user.service.ts
@Injectable()
export class UserService {}

And our UserService which is completely empty.

Registration

The first thing that we want to implement is our registration method. So it will be a registration method were we provide data for registration. Typically to specify the data of the body we create a DTO class. In order to do that we must install an additional package.

npm i class-validator

Now we want to create a class which will validate all properties that we can pass in our registration method.

// src/user/dto/createUser.dto.ts
import {IsEmail, IsNotEmpty} from "class-validator"

export class CreateUserDto {
  @IsNotEmpty()
  readonly username: string
  @IsNotEmpty()
  @IsEmail()
  readonly email: string
  @IsNotEmpty()
  readonly password: string
}

Here we create a DTO with fields username, email and password that we must provide in our body. As you can see from class-validator we used IsEmail and isNotEmpty to add validations to every field. Now let's add a registration method in our controller.

// src/user/user.controller.ts
@Controller()
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post('users')
    async createUser(
      @Body() createUserDto: CreateUserDto
    ): Promise<any> {
      console.log('createUserDto', createUserDto)
      return 'createUser'
    }
  }

Here is our POST method with body which we specified as CreateUserDto that we just created. Don't worry that we returned Promise<any> this is just for testing. We won't have any in our application. We just write a console.log and a return inside to check if our method is working.

Basic register

Now what we want to do is to call a service method which will hold all the logic.

// src/user/user.controller.ts
@Post('users')
  async createUser(
    @Body() createUserDto: CreateUserDto
  ): Promise<any> {
    const user = await this.userService.createUser(createUserDto)
    return 'createUser'
  }
}

Why don't we write any logic directly in our controller?

The purpose of the controller to build a response. The purpose of service is to do all logic.

// src/user/user.service.ts
export class UserService {
  async createUser(createUserDto: CreateUserDto): Promise<UserEntity> {
  }
}

Now all logic that we need for registering a user we will write inside this method in the service.

Configuring database

But we didn't configure a database inside our application. I already have a MongoDB which is running which means I must now install correct packages to configure my NestJS application.

npm i mongoose @nestjs/mongoose

Mongoose is a wrapper for MongoDB to work with the database and @nestjs/mongoose makes bindings for MongoDB inside NestJS.

After this we must configure MongoDB in our project.

// src/app.module.ts
...
import {MongooseModule} from '@nestjs/mongoose';
@Module({
  imports: [UserModule, MongooseModule.forRoot('mongodb://localhost/app')],
  controllers: [],
  providers: [],
})
export class AppModule {
  ...
}

Here we defined how our application must be connected to Mongoose. But we are still missing a user entity for our user feature. Why do we need user entity? We need to have some class which can make all these different requests to the database like save record, get record and so on.

// src/user/user.entity.ts
import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose";

@Schema()
export class UserEntity {
  @Prop()
  email: string

  @Prop()
  username: string

  @Prop({select: false})
  password: string
}

export const UserEntitySchema = SchemaFactory.createForClass(UserEntity)

Here we created a UserEntity class based on which we create Mongoose schema. We specify all properties that our User will have in database. It is important to mention that for property password we put {select: false}. It is needed because we don't want to select password in any requests by default as it is a secret information and we don't want to expose it.

// src/user/user.module.ts
...
import {UserEntity, UserEntitySchema} from './user.entity';

@Module({
  imports: [MongooseModule.forFeature([{name: UserEntity.name, schema: UserEntitySchema}])],
  ...
})
export class UserModule {}

Now we register in our UserModule our UserEntity that we created so we can do request to the database with this user entity.

// src/user/user.service.ts
export class UserService {
  constructor(@InjectModel(UserEntity.name) private userModel: Model<UserEntity>) {}
  ...
}

In order to use our UserModel inside a service and do some requests we must inject it like this. Don't worry if it looks complicated. It will look like this in every single file.

// src/user/user.service.ts
export class UserService {
  ...
  async createUser(createUserDto: CreateUserDto): Promise<UserEntity> {
    const user = await this.userModel.findOne({email: createUserDto.email})

    if (user) {
      throw new HttpException('Email is already taken', HttpStatus.UNPROCESSABLE_ENTITY)
    }

    const createdUser = new this.userModel(createUserDto)
    return createdUser.save()
  }
}

We used a model to try and find user first in our createUser method. It we found a user with such email we throw an exception. After this we create a new user and save to the database.

// src/user/user.controller.ts
@Post('users')
async createUser(
  @Body() createUserDto: CreateUserDto
): Promise<any> {
  const user = await this.userService.createUser(createUserDto)
  return user
}

Now as we returned a user from our service we want to render data on the screen.

Finished register

As you can see now we can create a user and we get these data back. But here I want to point out that our password was not hashed and just stored as it is. This is not safe and we should never do that.

Hashing password

This is why we must fix it as soon as possible. In order to hash a password we typically use a bcrypt library.

npm i bcrypt

Now we can go to our UserEntity and create some logic to hash password before saving.

// src/user/user.entity.ts
...
import {hash} from "bcrypt";

@Schema()
export class UserEntity {
  ...
}

export const UserEntitySchema = SchemaFactory.createForClass(UserEntity)

UserEntitySchema.pre<UserEntity>('save', async function (next: Function) {
  this.password = await hash(this.password, 10)
  next()
})

We added UserEntitySchema.pre which allows us to do some logic before save. What we are doing here is replacing a password with a hashed version.

Hashed password

As you can see now when we create a user our password is correctly hashed.

User response

Here I want to point our one problem that we have. If we just to UserService we can see that we return a UserEntity back. UserEntity contains all fields of our user. This is not the great way to respond in our API as we don't control what fields we want to send back.

This is why it makes a lot of sense to create an additional type and method to generate a user response that we want.

// src/user/types/userResponse.type.ts
import {UserEntity} from "../user.entity";

export type UserResponseType = Omit<UserEntity, 'password'>

Here we created a new data type which doesn't contain a password. Now we need to create an additional function which will transform our user.

// src/user/user.service.ts
export class UserService {
  ...
  buildUserResponse(userEntity: UserEntity): UserResponseType {
    return {
      username: userEntity.username,
      email: userEntity.email,
    }
  }
}

This function buildUserResponse will transform our UserEntity to UserResponseType. Not we can use this function inside a controller to build the correct response.

// src/user/user.controller.ts
@Post('users')
async createUser(
  @Body() createUserDto: CreateUserDto
): Promise<UserResponseType> {
  const user = await this.userService.createUser(createUserDto)
  return this.userService.buildUserResponse(user)
}

So we used now this function in the controller and additionally replaced any to UserResponseType.

Build response

As you can see all fields except of needed were stripped out.

Login

Our next step here is to implement loginning of the user. Actually it will be super similar.

// src/user/user.controller.ts
...
@Post('users/login')
async login(
  @Body() loginDto: LoginDto
): Promise<UserResponseType> {
  const user = await this.userService.loginUser(loginDto)
  return this.userService.buildUserResponse(user)
}

We have a route /users/login with loginDto that we need to create and we call loginUser in the service to get user information. Let's create a LoginDto first.

// src/user/dto/login.dto.ts
import {IsEmail, IsNotEmpty} from "class-validator"

export class LoginDto {
  @IsNotEmpty()
  @IsEmail()
  readonly email: string
  @IsNotEmpty()
  readonly password: string
}

Our LoginDto has just 2 fields email and password. Now we just need to implement userService.loginUser method.

// src/user/user.service.ts
import {compare} from 'bcrypt';
...
export class UserService {
  async loginUser(loginDto: LoginDto): Promise<UserEntity> {
    const user = await this.userModel.findOne({email: loginDto.email}).select('+password')

    if (!user) {
      throw new HttpException('User not found', HttpStatus.UNPROCESSABLE_ENTITY)
    }

    const isPasswordCorrect = await compare(loginDto.password, user.password)

    if (!isPasswordCorrect) {
      throw new HttpException('Incorrect password', HttpStatus.UNPROCESSABLE_ENTITY)
    }

    return user
  }
}

First of all we try to find a user by email. But most importantly we want to select additionally our password. We need that to compare the provided password with stored password. If we don't find a user we throw an error. After this we check if password is the same by using compare function from bcrypt. Only if the password is correct we return a user.

Login

Now we can send a POST request to login a user with email and password and we get user data back.

JWT Token

Now we must talk about JWT token. Why is that? We we login or register a user we want to return a JWT token for the client so it can authenticate itself later. This is how it works.

We provide a unique string (JWT token) from the backend, client stores it and uses later to authenticate their requests. So client must provide this stored JWT token back inside Headers. Backend parses it and understands what user it was. So the first step here is to generate a JWT token.

npm i jsonwebtoken

Now the question is where do we want to generate a token? It is not related to database at all so this buildUserResponse method looks like a nice place to generate it.

// src/user/user.service.ts
import {sign} from 'jsonwebtoken';
...
export class UserService {
  buildUserResponse(userEntity: UserEntity): UserResponseType {
    return {
      username: userEntity.username,
      email: userEntity.email,
      token: this.generateJwt(userEntity)
    }
  }

  generateJwt(userEntity: UserEntity): string {
    return sign({email: userEntity.email}, 'JWT_SECRET')
  }
}

We created an additional generateJwt method which creates this unique token by using sign function from jsonwebtoken. As a second parameter to sign we must provide a unique secret string. It is important that only our backend know this secret string so nobody else can read it.

Our buildUserResponse return type is not valid now because UserResponseType doesn't know about token property.

// src/user/types/userResponse.type.ts
import {UserEntity} from "../user.entity";

export type UserResponseType = Omit<UserEntity, 'password'> & {token: string}

Now we added a token property to our UserResponseType.

token

Now when we login or register a user we generate additionally a JWT token for our client.

Getting a user

Now just imagine that our client made a login request, obtained a token and stored it in local storage. Then after page reload client wants to authenticate a user again with this token. It must do a request to get a user and provide a token in the headers.

But we don't want to just parse a token from client in our get current user request. Realistically we want to parse it on any request to check if client is allows to get data that it requested. In NestJS we implement this with a middleware.

// src/user/middlewares/auth.middleware.ts
import {Injectable, NestMiddleware} from "@nestjs/common";
import {NextFunction, Request, Response} from "express";
import {UserEntity} from "../user.entity";
import {verify} from "jsonwebtoken";
import {UserService} from "../user.service";

export interface ExpressRequest extends Request {
  user?: UserEntity
}

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private userService: UserService) {}

  async use(req: ExpressRequest, res: Response, next: NextFunction) {
    if (!req.headers['authorization']) {
      req.user = null
      next()
      return
    }

    const token = req.headers['authorization'].split(' ')[1]

    try {
      const decode = verify(token, 'JWT_SECRET') as {email: string}
      const user = await this.userService.findByEmail(decode.email)
      req.user = user
      next()
    } catch (err) {
      req.user = null
      next()
    }
  }
}

Here we created an AuthMiddleware class which must implement NestMiddleware. It means that we must create a use method inside. In use we get 3 parameters req, res and next. We can read or attach some data to the request and we call next when we want our request to continue.

It is important that a default request doesn't contain a user inside it. This is why we extend it and create an ExpressRequest.

First we check if authorization header is there. If not then we set a user to null which means user is not authorized and we call next to continue.

In other case we try to get a token to we call verify with our secret string to decode it and get user information back. It allows us to get a user from the database with findByEmail method. Then we attach this user to req.user so it is accessible for us inside controller.

But we didn't create findByEmail method in our service yet.

// src/user/user.service.ts
export class UserService {
  ...
  async findByEmail(email: string): Promise<UserEntity> {
    return this.userModel.findOne({email})
  }

The last step is to register our middleware globally for the whole application.

// src/app.module.ts
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(AuthMiddleware).forRoutes({
      path: '*',
      method: RequestMethod.ALL
    })
  }
}

It means that for any request we want to call our middleware first.

Now we can start implementing getting of current user in our controller.

@Controller()
export class UserController {
  @Get('user')
  async currentUser(@Request() request: ExpressRequest): Promise<UserResponseType> {
    if (!request.user) {
      throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED)
    }
    return this.userService.buildUserResponse(request.user)
  }
}

Here we created a /user request which has full access to our request with @Request decorator. If our user is not there we throw 401 error. In other way to respond with user information because we have a full entity in the request.

result

Here we provided Token our-unique-token in the header and we got a current user information. Which means that we successfully build an authentication in NestJS.

And actually if you want to learn NestJS from empty folder to a fully functional production application make sure to check my NestJS - Building Real Project From Scratch course.

📚 Source code of what we've done