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.
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.
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.
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
.
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.
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
.
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.
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.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done