Adding authentication to a REST API is a pretty common practice in API development. The method of choice is usually JWT authentication. In this tutorial, we’re going to go over adding JWT authentication to Nest.js using TypeORM and Passport. If you’re interested in the video tutorial and more, please click here. You can also find the github repo here.

Configuration

We’ll start out by adding some of the dependencies

npm install -save @nestjs/jwt @nestjs/passport @nestjs/swagger @nestjs/typeorm @types/bcryptjs
bcrypt jsonwebtoken passport passport-jwt passport-local pg swagger-ui-express typeorm

These should add the dependencies required for JWT creation, the TypeORM provider, bcrypt for hashing passwords and passport for our authentication strategies.

npm install -s --dev @types/bcrypt @types/jsonwebtoken @types/passport-jwt
@types/passport-local

This will install the @types files for some of your dependencies. This is to help your IDE determine the typings for those dependencies.

User Domain Creation

We’ll next want to create our domain for our Users. This included the entity for our ORM and how to hash the user passwords. First lets generate that domain.

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

Next, we will create our Response Object as a users.ro.ts file.

export class UserRO {
    id: number;
    email: string;
    firstName: string;
    lastName: string;
}

Next, we will create our TypeORM entity users.entity.ts.

import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    CreateDateColumn,
    BeforeInsert,
    OneToMany,
    ManyToMany,
    JoinTable,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
import { UserRO } from './users.ro';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column()
  email: string;

  @Column()
  password: string;

  @BeforeInsert()
  async hashPassword() {
    this.password = await bcrypt.hash(this.password, 10);
  }

  async comparePassword(attempt: string): Promise<boolean> {
    return await bcrypt.compare(attempt, this.password);
  }

  toResponseObject(showToken: boolean = true): UserRO {
    const { id, firstName, lastName, email } = this;
    const responseObject: UserRO = {
      id,
      firstName,
      lastName,
      email,
    };

    return responseObject;
  }
}

We have a method to hash the password before it is saved and another to compare when it’s being authenticated. We’ll then create our DTO for swagger.

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;
}

User Service

Now that we have our Entity and DTOs squared away, we can now create our service and module. We’ll first start with a crud service that also allows for us to register the user.

import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DeleteResult } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto } from './users.dto';
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  public async findAll(): Promise<User[]> {
    return await this.userRepository.find();
  }

  public async findByEmail(userEmail: string): Promise<User | null> {
    return await this.userRepository.findOne({ email: userEmail });
  }

  public async findById(id: number): Promise<User | null> {
    return await this.userRepository.findOneOrFail(id);
  }

  public async create(user: CreateUserDto): Promise<User> {
    return await this.userRepository.save(user);
  }

  public async update(
    id: number,
    newValue: CreateUserDto,
  ): Promise<User | null> {
    const user = await this.userRepository.findOneOrFail(id);
    if (!user.id) {
      // tslint:disable-next-line:no-console
      console.error("user doesn't exist");
    }
    await this.userRepository.update(id, newValue);
    return await this.userRepository.findOne(id);
  }

  public async delete(id: number): Promise<DeleteResult> {
    return await this.userRepository.delete(id);
  }

  public async register(userDto: CreateUserDto): Promise<User> {
    const { email } = userDto;
    let user = await this.userRepository.findOne({ where: { email } });
    if (user) {
      throw new HttpException(
        'User already exists',
        HttpStatus.BAD_REQUEST,
      );
    }
    user = await this.userRepository.create(userDto);
    return await this.userRepository.save(user);
  }
}

Then we will create a module that registers our TypeORM entity.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

Auth Service Generation

Now that we have our UserService, now we have to create a service for authenticating our endpoints. First, we will start off by generating some of our files.

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

Then we will create an interface folder where we create contract interfaces for our payloads. These will be named jwt-payload.interface.ts, registrationStatus.interface.ts, and token.interface.ts.

export interface JwtPayload {
  id: number;
  email: string;
  firstName: string;
  lastName: string;
}

export interface RegistrationStatus {
  success: boolean;
  message: string;
}

export interface IToken {
  readonly token: string;
}

Auth Service

Next, we will create an Auth Service and DTO.

import * as jwt from 'jsonwebtoken';
import { Injectable, Logger } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
import { JwtPayload } from './interfaces/jwt-payload.interface';
import { User } from 'src/users/user.entity';
import { UserRO } from 'src/users/users.ro';
import { debug } from 'console';
import { RegistrationStatus } from './interfaces/registrationStatus.interface';
import { CreateUserDto } from 'src/users/users.dto';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  private readonly logger = new Logger(AuthService.name);
  
  async register(user: CreateUserDto) {
    let status: RegistrationStatus = {
      success: true,
      message: 'user register',
    };
    try {
      await this.usersService.register(user);
    } catch (err) {
      //debug(err);
      status = { success: false, message: err };
    }
    return status;
  }
  createToken(user: User) {
    //debug('get the expiration');
    const expiresIn = 3600;
    //debug('sign the token');
    //debug(user);

    const accessToken = jwt.sign(
      {
        id: user.id,
        email: user.email,
        firstname: user.firstName,
        lastname: user.lastName,
      },
      'Codebrains',
      { expiresIn },
    );
    //debug('return the token');
    //debug(accessToken);
    return {
      expiresIn,
      accessToken,
    };
  }

  async validateUserToken(payload: JwtPayload): Promise<User> {
    return await this.usersService.findById(payload.id);
  }
  async validateUser(email: string, password: string): Promise<UserRO> {
    const user = await this.usersService.findByEmail(email);
    if (user && user.comparePassword(password)) {
      this.logger.log('password check success');
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}
import { ApiModelProperty } from '@nestjs/swagger';

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

    @ApiModelProperty()
    readonly password: string;
}

This auth service will allow us to create two types of strategies. One for a local authentication strategy (username and password) and the other for a JWT strategy. These can be names local.strategy.ts and jwt.strategy.ts.

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

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      usernameField: 'email',
      passwordField: 'password',
    });
  }

  private readonly logger = new Logger(AuthService.name);

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    this.logger.log(user);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Using the local strategy, we can set the email property to the username field when trying to login. As it sounds, this makes your email address your username. The validate function is necessary when checking if the user exists and their password is correct.

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

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

  async validate(payload: any, done: Function) {
    const user = await this.authService.validateUserToken(payload);
    if (!user) {
      return done(new UnauthorizedException(), false);
    }
    done(null, user);
  }
}

For the JWT strategy, it checks if the token is valid such as whether it is expired.

Inside of your module, you have to register your strategies and it’s also another place to set your token secret.

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: 'Codebrains',
      signOptions: { expiresIn: 3600 },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule {}

Now in order to login or register, you must add the correct endpoints to your controller. These will use the local strategy auth guard.

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

@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);
  }

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

Testing Nest.js Auth Endpoint

Now that we have most of the app configured we still have to make sure the ORM is properly configured. Make sure TypeORM is in your app module.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(), AuthModule, UsersModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Set your ormconfig.json

{
    "type": "postgres",
    "host": "localhost",
    "port": 5432,
    "username": "jamescoonce",
    "password": "",
    "database": "NestTypeORMAuth",
    "entities": [
        "src/**/**.entity{.ts,.js}"
    ],
    "synchronize": true
}

Now set the JWT auth guard on an endpoint.


import { Controller, Get, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { AuthGuard } from '@nestjs/passport';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @UseGuards(AuthGuard('jwt'))
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

Authenticate your user after registering.

Nest.js Login Request

Now make a request to your endpoint.

Conclusion

Now that you’ve learned how to add JWT Authentication to Nest.js with TypeORM, try using it in a full project. Don’t be shy, try watching a few of our videos here. You can learn about authentication as well as building out larger-scale projects. Also, try reading a similar article on using MongoDB and Mongoose.

Codebrains Newsletter

Get weekly dev news and tutorials.

Powered by ConvertKit