In a previous post, we talked about using JWT authentication with Express JS. In this tutorial, we’ll go over using it with Typescript and NestJS. This tutorial goes over using different NestJS plugins and middleware for JWT Authentication. If you’re interested in learning more, take a look at our GitHub project or review some of our videos.

NestJS JWT Dependencies

The project setup is a continuation of our post on building a restful API with NestJS. Once you have your REST API setup, you’ll want to add dependencies for passport and JWT authentication.

npm install @nestjs/passport @types/joi @types/passport-local-mongoose joi jsonwebtoken
passport passport-http-bearer passport-jwt passport-local passport-local-mongoose

These dependencies add access to validating JWT with passport, validating user credential and signing up users and storing them in MongoDB.

Creating Our User Model

We’ll start off by creating our Users domain. This will hold our Controllers, Module and Models.

nest generate module users 
nest generate controller users
nest generate service users

Make sure to move your UsersService into the users folder. Next, we will create our DTOs. Create a dto folder under users and create 2 files. createUser.dto.ts and loginUser.dto.ts

import { ApiModelProperty } from '@nestjs/swagger';

export class CreateUserDto {
    @ApiModelProperty()
    readonly _id: number;

    @ApiModelProperty()
    readonly firstName: string;

    @ApiModelProperty()
    readonly lastName: string;

    @ApiModelProperty()
    readonly email: string;

    @ApiModelProperty()
    readonly password: string;
}
import { ApiModelProperty } from '@nestjs/swagger';

export class LoginUserDto {
    @ApiModelProperty()
    readonly email: string;

    @ApiModelProperty()
    readonly password: string;
}

As you can see, we also annotate our classes for swagger documentation. That way, users can know what parameters to use when sending post requests.

Passport User Model

We will also want to create a model to be stored in our database so let’s create our scheme for our MongoDB users model. Create a folder called ‘schemas’ under users and then create a user.schema.ts file.

import * as mongoose from 'mongoose';
import * as passportLocalMongoose from 'passport-local-mongoose';

export const UserSchema = new mongoose.Schema({
    firstName: String,
    lastName: String,
    email: String,
    password: String,
});
UserSchema.plugin(passportLocalMongoose);

You can see that we’ve imported the ‘passportLocalMongoose’ plugin. This allows for us to have an already configured setup of hashing our passwords for MongoDB. Even though we could create this logic ourselves, it’s better to follow DRY and use a package that’s already been tested by the community.

NestJS Services and Validation

When our users sign-up and login, we will want to make sure that their credentials conform to our standards for emails and passwords. This is where joi comes in. This is a package for validating our models. Under the users directory, create a folder called ‘joi’. Then create a auth-user.joi.ts file.

import { object, string, ObjectSchema } from 'joi';

export const authUserSchema: ObjectSchema = object({
    email: string().email().required(),
    password: string().alphanum().min(6).max(36).required(),
});

What this does is require that both email and password be required, make sure emails are actual emails and that passwords have a minimum and maximum length.

Users Service

Our users service isn’t too complicated. We’ll create two interfaces. user.interface.ts and iusers.service.ts. The IUser interface will specify what properties our MongoDB user will have and our IUsersService will specify what data our actual service should return.

import { Document, PassportLocalDocument } from 'mongoose';

export interface IUser extends PassportLocalDocument {
    readonly firstName: string;
    readonly lastName: string;
    readonly email: string;
    readonly password: string;
}
import { IUser } from './user.interface';

export interface IUsersService {
    findAll(): Promise<IUser[]>;
    findById(ID: number): Promise<IUser | null>;
    findOne(options: object): Promise<IUser | null>;
    create(user: IUser): Promise<IUser>;
    update(ID: number, newValue: IUser): Promise<IUser | null>;
    delete(ID: number): Promise<string>;
}

One that is done, inside of your users.service.ts file, create a CRUD service.

import { Model, PassportLocalModel } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { debug } from 'console';
import { IUsersService } from './interfaces/iusers.service';
import { IUser } from './interfaces/user.interface';
import { CreateUserDto } from './dto/createUser.dto';

@Injectable()
export class UsersService implements IUsersService {
    constructor(@InjectModel('User') private readonly userModel: PassportLocalModel<IUser>) {}
    async findAll(): Promise<IUser[]> {
        return await this.userModel.find().exec();
    }

    async findOne(options: object): Promise<IUser> {
        return await this.userModel.findOne(options).exec();
    }

    async findById(ID: number): Promise<IUser> {
        return await this.userModel.findById(ID).exec();
    }
    async create(createUserDto: CreateUserDto): Promise<IUser> {
        const createdUser = new this.userModel(createUserDto);
        return await createdUser.save();
    }

    async update(ID: number, newValue: IUser): Promise<IUser> {
        const user = await this.userModel.findById(ID).exec();

        if (!user._id) {
            debug('user not found');
        }

        await this.userModel.findByIdAndUpdate(ID, newValue).exec();
        return await this.userModel.findById(ID).exec();
    }
    async delete(ID: number): Promise<string> {
        try {
            await this.userModel.findByIdAndRemove(ID).exec();
            return 'The user has been deleted';
        }
        catch (err) {
            debug(err);
            return 'The user could not be deleted';
        }
    }
}

Configuring Out UserModule

As the last step for our users service, we’ll configure our module. This connects all of our providers, models and controllers to our NestJS applications.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSchema} from './schemas/user.schema';

@Module({
    imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
    controllers: [UsersController],
    providers: [UsersService],
    exports: [UsersService, MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
})
export class UsersModule {}

Creating Our Auth Modules

We will do the same process for creating our auth services and controllers.

nest generate module auth 
nest generate controller auth
nest generate service auth

Also create an interfaces folder and a passport folder inside of our auth folder.

JWT Auth Interfaces

We’ll create 3 interfaces. jwt-payload.interface.ts , registrationStatus.interface.ts and token.interface.ts

export interface JwtPayload {
    id: number;
    email: string;
}
export interface RegistrationStatus {
    success: boolean;
    message: string;
}
export interface IToken {
    readonly token: string;
}

JWT Auth Service

We’ll need to create a service for getting our users from the database and creating jwt(JSON Web Tokens). We’ll have to import our UsersService as well as our package for signing our tokens.

import * as jwt from 'jsonwebtoken';

import { UsersService } from '../users/users.service';

We will also need to inject our User model and Users Service.

constructor(private readonly usersService: UsersService,
                @InjectModel('User') private readonly userModel: PassportLocalModel<IUser>) { }

The most import portions are a method to register the user, validate the user and generate the token.

import * as jwt from 'jsonwebtoken';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtPayload } from './interfaces/jwt-payload.interface';
import { Model, PassportLocalModel } from 'mongoose';
import { IUser } from '../users/interfaces/user.interface';
import { InjectModel } from '@nestjs/mongoose';
import { debug } from 'console';
import { RegistrationStatus } from './interfaces/registrationStatus.interface';

@Injectable()
export class AuthService {
    constructor(private readonly usersService: UsersService,
                @InjectModel('User') private readonly userModel: PassportLocalModel<IUser>) { }

    async register(user: IUser) {
        let status: RegistrationStatus = { success: true, message: 'user register' };
        await this.userModel.register(new this.userModel({username: user.email,
            firstName: user.firstName,
            lastName: user.lastName}), user.password, (err) => {
            if (err) {
                debug(err);
                status = { success: false, message: err };
            }
        });
        return status;
    }

    createToken(user) {
        console.log('get the expiration');
        const expiresIn = 3600;
        console.log('sign the token');
        console.log(user);

        const accessToken = jwt.sign({ id: user.id,
            email: user.username,
            firstname: user.firstName,
            lastname: user.lastName }, 'ILovePokemon', { expiresIn });
        console.log('return the token');
        console.log(accessToken);
        return {
            expiresIn,
            accessToken,
        };
    }
    async validateUser(payload: JwtPayload): Promise<any> {
        return await this.usersService.findById(payload.id);
    }
}

NestJS JWT Passport Configuration

In order to get Passport to work, we will have to setup two strategies. A Local Strategy and a JWT Strategy. These allow for 2 different type or authentication processes needed for our application, Username and password auth which will return a JWT and JWT auth which will let us access our RESTful resources.

Passport LocalStrategy for NestJS

The setup for the LocalStrategy is simply. Extend your class from PassportStrategy and place ‘passport-local’ ass the selected Strategy. You can see the @typings to see how injecting different strategies work.

import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
import { Model, PassportLocalModel } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { IUser } from '../../users/interfaces/user.interface';
import { UserSchema } from '../../users/schemas/user.schema';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
    constructor(private readonly authService: AuthService,
                @InjectModel('User') private readonly userModel: PassportLocalModel<IUser>) {
        super({
            usernameField: 'email',
            passwordField: 'password',
        }, userModel.authenticate());
    }
}

You can see that you specify which fields to use for Local Authentication and which model to authenticate.

Passport JWTStrategy for NestJS

Setting up the JWTStrategy is a little more confusing. You have to create a method to validate the token as well as verify that there is a token in the request. You do this by extracting the token from the request.

import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from '../auth.service';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { use } from 'passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtPayload } from '../interfaces/jwt-payload.interface';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor(private readonly authService: AuthService) {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            secretOrKey: 'ILovePokemon',
        });
    }

    // tslint:disable-next-line:ban-types
    async validate(req: Request, payload: JwtPayload, done: Function) {
        const user = await this.authService.validateUser(payload);
        if (!user) {
            return done(new UnauthorizedException(), false);
        }
        done(null, user);
    }
}

Authorization Controller Endpoints

Now that we have our services setup, we can now use them in our controller. We’ll have two endpoints. One for logging the user in and the other for Registering the user.

import { Controller, UseGuards, HttpStatus, Response, Request, Get, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateUserDto } from '../users/dto/createUser.dto';
import { LoginUserDto } from '../users/dto/loginUser.dto';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
import { ApiUseTags, ApiResponse } from '@nestjs/swagger';
import { RegistrationStatus } from './interfaces/registrationStatus.interface';
import { UsersService } from '../users/users.service';
import { Inject } from '@nestjs/common';
import { debug } from 'util';

@ApiUseTags('auth')
@Controller('auth')
export class AuthController {
    constructor(private readonly authService: AuthService,
                private readonly usersService: UsersService) {}

    @Post('register')
    public async register(@Response() res, @Body() createUserDto: CreateUserDto){
        const result = await this.authService.register(createUserDto);
        if (!result.success){
            return res.status(HttpStatus.BAD_REQUEST).json(result);
        }
        return res.status(HttpStatus.OK).json(result);
    }

    @Post('login')
    @UseGuards(AuthGuard('local'))
    public async login(@Response() res, @Body() login: LoginUserDto){
        return await this.usersService.findOne({ username: login.email}).then(user => {
            if (!user) {
                res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
                    message: 'User Not Found',
                });
            } else {
                console.log('start getting the token');
                const token = this.authService.createToken(user);
                console.log(token);
                return res.status(HttpStatus.OK).json(token);
            }
        });
    }
}

You can see in our login route, we have a guard that doesn’t allow the user access unless they use the LocalStrategy to authenticate.

Auth Module

Inside of our AuthModule, we’ll import our UserService as well as register all of our providers and exports.

import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
} from '@nestjs/common';

import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { bodyValidatorMiddleware } from './middlewares/body-validator.middleware';
import { UsersModule } from '../users/users.module';
// Strategies
import { JwtStrategy } from './passport/jwt.strategy';
import { LocalStrategy } from './passport/local.strategy';

import { authenticate } from 'passport';

@Module({
  imports: [UsersModule],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, LocalStrategy],
  exports: [AuthService, JwtStrategy, LocalStrategy],
})
export class AuthModule {}

Testing our NestJS JWT Authentication

Once the application is setup, you should be able to both register and login a user.

"<yoastmark

"<yoastmark

"<yoastmark

Conclusion

Adding JWT Authentication to NestJS is simple based on all of the @typings and middleware that streamline the process. It helps when guarding against unauthorized users. You can futher restrict access to your API by adding a permissions and roles system. If you’re interested in learning more, please review some of our videos.

Codebrains Newsletter

Get weekly dev news and tutorials.

Powered by ConvertKit