4 February, 2018

Authentication with Passport JWT

This is the third part of Getting Started with NestJS. This document was updated to use NestJS 5.3.6

We use Passport as our authentication middleware with NestJS. It suppport different methods, in Passport it's called Strategy, to authenticate e.g Local, OpenID, Facebook, Google Account and Twitter. We use the local one.

We need a user entity to persist registered user. This is the same task as creating the product entity from last time.

Create folder user in folder nestjs-backend/src.

Create file user.entity.ts in folder nestjs-backend/src/user.

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column({ length: 50, unique: true })
  username: string;

  @Column({ length: 100, nullable: true })
  password: string|undefined;

  @Column({ length: 100, nullable: true })
  passwordHash: string|undefined;

  @Column({ length: 500 })
  email: string;
}

The password won't be save in the database, it well always be empty. We hash it and save that instead.

Add bcrypt as a project dependency with npm i -s bcrypt

Create file user.service.ts in folder nestjs-backend/src/user.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UserService {
  private saltRounds = 10;

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>) {}

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

  async getUserByUsername(username: string): Promise<User> {
    return (await this.userRepository.find({ username }))[0];
  }

  async createUser(user: User): Promise<User> {
    user.passwordHash = await this.getHash(user.password);

    // clear password as we don't persist passwords
    user.password = undefined;
    return this.userRepository.save(user);
  }

  async getHash(password: string|undefined): Promise<string> {
      return bcrypt.hash(password, this.saltRounds);
  }

  async compareHash(password: string|undefined, hash: string|undefined): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }
}

Create file user.module.ts in folder nestjs-backend/src/user.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule {}

Update app.module.ts to reference our UserModule.

  imports: [
  ..., UserModule]

Passport with JWT

Install passport, passport-jwt and jsonwebtoken as dependency. ```sh npm i -s passport passport-jwt jsonwebtoken ``` Create folder `auth` in `nestjs-backend/src`.

Create file auth.service.ts in nestjs-backend/src/auth.

import * as jwt from 'jsonwebtoken';
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';

@Injectable()
export class AuthService {
  constructor(private readonly userService: UserService) { }

  async createToken(id: number, username: string) {
    const expiresIn = 60 * 60;
    const secretOrKey = 'secret';
    const user = { username };
    const token = jwt.sign(user, secretOrKey, { expiresIn });

    return { expires_in: expiresIn, token };
  }

  async validateUser(signedUser): Promise<boolean> {
    if (signedUser&&signedUser.username) {
      return Boolean(this.userService.getUserByUsername(signedUser.username));
    }

    return false;
  }
}

Create file auth.controller.ts in folder nestjs-backend/src/auth.

import { Controller, Post, HttpStatus, HttpCode, Get, Response, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserService } from '../user/user.service';
import { User } from '../user/user.entity';

@Controller('auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly userService: UserService) {}

  @Post('login')
  async loginUser(@Response() res: any, @Body() body: User) {
    if (!(body && body.username && body.password)) {
      return res.status(HttpStatus.FORBIDDEN).json({ message: 'Username and password are required!' });
    }

    const user = await this.userService.getUserByUsername(body.username);
    
    if (user) {
      if (await this.userService.compareHash(body.password, user.passwordHash)) {
        return res.status(HttpStatus.OK).json(await this.authService.createToken(user.id, user.username));
      }
    }

    return res.status(HttpStatus.FORBIDDEN).json({ message: 'Username or password wrong!' });
  }

  @Post('register')
  async registerUser(@Response() res: any, @Body() body: User) {
    if (!(body && body.username && body.password)) {
      return res.status(HttpStatus.FORBIDDEN).json({ message: 'Username and password are required!' });
    }

    let user = await this.userService.getUserByUsername(body.username);

    if (user) {
      return res.status(HttpStatus.FORBIDDEN).json({ message: 'Username exists' });
    } else {
      user = await this.userService.createUser(body);
      if (user) {
        user.passwordHash = undefined;
      }
    }

    return res.status(HttpStatus.OK).json(user);
  }
}

Create folder passport in nestjs-backend/src/auth.

Create file jwt.strategy.ts in folder nestjs-backend/src/auth.

import * as passport from 'passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth.service';

@Injectable()
export class JwtStrategy extends Strategy {
  constructor(private readonly authService: AuthService) {
    super(
      {
        jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
        passReqToCallback: true,
        secretOrKey: 'secret',
      },
      async (req, payload, next) => await this.verify(req, payload, next)
    );
    passport.use(this);
  }

  public async verify(req, payload, done) {
    const isValid = await this.authService.validateUser(payload);
    if (!isValid) {
      return done('Unauthorized', false);
    }
    done(null, payload);
  }
}

Create file auth.module.ts in folder nestjs-backend/src/auth.

import * as passport from 'passport';
import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtStrategy } from './passport/jwt.strategy';
import { AuthController } from './auth.controller';
import { UserModule } from '../user/user.module';

@Module({
  imports: [UserModule],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule implements NestModule {
  public configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(passport.authenticate('jwt', { session: false }))
      .forRoutes(
        { path: '/products', method: RequestMethod.ALL },
        { path: '/products/*', method: RequestMethod.ALL });
  }
}

Update app.module.ts to reference our AuthModule.

import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
  ..., AuthModule]

Open the file rest-test.http and add the following to the bottom:

###
POST http://localhost:3000/auth/register
Content-Type: application/json

{ "username" : "less", "email": "less@outlook.com", "password": "topsecrete"}

###
POST http://localhost:3000/auth/login
Content-Type: application/json

{ "username": "less", "password": "topsecrete"}

###
POST http://localhost:3000/products
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNTE3NjczNTM4LCJleHAiOjE1MTc2NzcxMzh9.JOlDi241tAw-zyQTMJU6Y5EwlMmvOnIpD2NCvFdkALs
Content-Type: application/json

{"name":"product3", "description": "Third Product"}

The first link is to get the access token. The token will expire in 3600 seconds. More about jsonwebtoken https://github.com/auth0/node-jsonwebtoken

The second link is to access the protected resource using the access token. Replace the text after Bearer with the fresh token.

Now restart npm start and click on the Send Request link generated by the rest client.

Update product.service.ts:

  async createProduct(product: Product): Promise<Product> {
    return this.productRepository.save(product);
  }

Update products.controller.ts:

    @Post()
    createProduct(@Body() body: Product) {
      if (body && body.name && body.description) {
        return this.productService.createProduct(body);
      }
    }

I have put the source code at GitHub