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