Authentication in NestJS — PostgreSQL and JWT

Rui Coelho
8 min readApr 3, 2021

--

We already saw that it is really is to develop an REST Api using NestJs, although, how easy is to implement authentication in NestJS using JWT? Spoiler alert: It’s really easy.

Let’s start

First we need to create our nest project. For this step I will a script developed by me that will generate a project with logger and TypeORM connection built in. If you need help to connect TypeORM with NestJS checkout my post about that. If you want know a little bit more about NestJS you can check my other post when I develop a NestJS REST Api from scratch.

Generate NestJS app with my script

Now it’s time to setup our postgres connection, as I mentioned before I have done it on a previous post so, for more details go check that post.

Install postgres on docker

If you used my script you only need to change .env file to setup database connection. I’m using my local database so only need to add this line to file:

DATABASE_URI = postgresql://postgres:nestjs@localhost:5432/nestjs
NODE_ENV = development
NPM_CONFIG_PRODUCTION = false
PORT = 3000

Now it’s time to start our application and check if no errors are given:

$ npm run dev

Create entities

Now we need to create our entities to define the login credentials. Let’s start by creating our database folder.

$ mkdir -p src/database/entities
$ touch src/database/entities/user.entity.ts

And add the following content to our user.entity.ts file:

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

@Entity()
export class Users {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: false, unique: true })
email: string;
@Column({ nullable: false })
password: string;
}

After that we need to import this on our src/app.module.ts :

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import configuration from './config/configuration';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from './database/entities/users.entity';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
url: configService.get('database.uri'),
entities: [Users],
synchronize: true,
}),
inject: [ConfigService],
}),
],
controllers: [],
providers: [],
})
export class AppModule {}

Once the base entity is defined we need to start creating our auth module.

Install authentication dependencies

The authencation module needs a couple of dependencies that we can install using npm:

$ npm install --save @nestjs/passport passport
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt

Authentication Module

We can use nestcli tool to generate our module, controller and service by doing:

$ nest g module auth
$ nest g service auth --no-spec
$ nest g controller auth --no-spec

We need to define our strategy folder and our DTO, we can do it by running:

$ mkdir -p src/auth/dto
$ mkdir -p src/auth/strategy
$ touch src/auth/dto/users.dto.ts
$ touch src/auth/strategy/jwt-auth.guard.ts
$ touch src/auth/strategy/jwt.strategy.ts

Let’s put this files on hold and deal with auth module first. On our auth module we need to define our certificates and sign options:

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { LoggerModule } from '../logger/logger.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategy/jwt.strategy';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
imports: [
PassportModule,
LoggerModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
return {
privateKey: configService.get<string>('keys.privateKey'),
publicKey: configService.get<string>('keys.publicKey'),
signOptions: { expiresIn: '60s', algorithm: 'RS256' },
};
},
inject: [ConfigService],
}),
],
providers: [JwtStrategy, AuthService],
exports: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}

The code above uses configService so we need to add our private and public key there. I will use our keys as environment variables, you can change that to use files or any other thing you want. I first run a bash command to export the generate keys and append them to .env file:

$ echo "PRIVATE_KEY=\""`awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' privateKey.pem`"\"" >> .env
$ echo "PUBLIC_KEY=\""`awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' publicKey.pem`"\"" >> .env

After this we only need to add 3 lines on our src/config/configuration.ts . The file should look like this:

export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
uri: process.env.DATABASE_URI,
},
keys: {
privateKey: process.env.PRIVATE_KEY.replace(/\\n/gm, '\n'),
publicKey: process.env.PUBLIC_KEY.replace(/\\n/gm, '\n'),
},
});

Now it’s a good time to start creating get code on our DTO file, this will help us validate content sent on request. One of the things I like to do for content validation is use a nice library called class-validator . We can install that using npm by running:

$ npm i --save class-validator

On our src/auth/dto/users.dto.ts we can now add:

import { IsEmail, IsString } from 'class-validator';

export class UsersDTO {
@IsEmail()
email: string;
@IsString()
password: string;
}

This defines our types to use on our validate function. Let’s start develop our controller, for this particular case we will only develop two endpoints, login and register. The register endpoint for demo purposes will be blocked with authentication, more about that later. On src/auth/auth.controller.ts :

import { Body, Controller, Post, Req, Res, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './strategy/jwt-auth.guard';

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

@Post('login')
async login(@Req() req, @Res() res, @Body() body) {
const auth = await this.authService.login(body);
res.status(auth.status).json(auth.msg);
}

@UseGuards(JwtAuthGuard)
@Post('register')
async register(@Req() req, @Res() res, @Body() body) {
const auth = await this.authService.createUser(body);
res.status(auth.status).json(auth.content);
}
}

Note: I already added @UseGuards(JwtAuthGuard) , this particular line will protect our register endpoint and force authenticated requests, but we will handle this later, for now let’s start login endpoint.

Login endpoint

Once the controller is defined we need to create our service, auth.service.ts it’s necessary to say that we want our passwords encrypted, so I will use bcrypt for this. To install bcrypt:

$ npm i --save bcrypt
$ npm i -D @types/bcrypt

Now, on our src/auth/auth.service.ts the following code:

import { Injectable } from '@nestjs/common';
import { UsersDTO } from './dto/users.dto';
import { validate } from 'class-validator';
import { JwtService } from '@nestjs/jwt';
import { LoggerService } from '../logger/logger.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Users } from '../database/entities/users.entity';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
constructor(
private logger: LoggerService,
private jwtService: JwtService,
@InjectRepository(Users) private usersRepository: Repository<Users>,
) {}

async login(user: any): Promise<Record<string, any>> {
// Validation Flag
let
isOk = false;

// Transform body into DTO
const
userDTO = new UsersDTO();
userDTO.email = user.email;
userDTO.password = user.password;

// Validate DTO against validate function from class-validator
await validate
(userDTO).then((errors) => {
if (errors.length > 0) {
this.logger.debug(`${errors}`, AuthService.name);
} else {
isOk = true;
}
});

if (isOk) {
// Get user information
const
userDetails = await this.usersRepository.findOne({
email: user.email,
});
if (userDetails == null) {
return { status: 401, msg: { msg: 'Invalid credentials' } };
}

// Check if the given password match with saved password
const
isValid = bcrypt.compareSync(user.password, userDetails.password);
if (isValid) {
return {
status: 200,
msg: {
email: user.email,
access_token: this.jwtService.sign({ email: user.email }),
},
};
} else {
return { status: 401, msg: { msg: 'Invalid credentials' } };
}
} else {
return { status: 400, msg: { msg: 'Invalid fields.' } };
}
}
}

Basically this method will transform the body sent on post request to our defined DTO, validate this against validate method from class-validator and after that check if the given password matches the stored on our database, this is basically our login endpoint.

Since we are using using UserRepository and LoggerService on our service we need to add them to our auth.module.ts imports, for this we need to add the following lines on our array of imports:

@Module({ imports: [..., LoggerModule, TypeOrmModule.forFeature([Users])], ...})

On the top we need to do some imports:

import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from '../database/entities/users.entity';

Time to check our console:

Example of expected output

Register Endpoint

To our register endpoint we can add the following code on src/auth/auth.service.ts :

async createUser(body: any): Promise<Record<string, any>> {
// Validation Flag
let
isOk = false;

// Transform body into DTO
const
userDTO = new UsersDTO();
userDTO.email = body.email;
userDTO.password = bcrypt.hashSync(body.password, 10);

// Validate DTO against validate function from class-validator
await validate
(userDTO).then((errors) => {
if (errors.length > 0) {
this.logger.debug(`${errors}`, AuthService.name);
} else {
isOk = true;
}
});
if (isOk) {
await this.usersRepository.save(userDTO).catch((error) => {
this.logger.debug(error.message, AuthService.name);
isOk = false;
});
if (isOk) {
return { status: 201, content: { msg: `User created with success` } };
} else {
return { status: 400, content: { msg: 'User already exists' } };
}
} else {
return { status: 400, content: { msg: 'Invalid content' } };
}
}

This code is already called on our controller, we can now test all this. If you added the annotation @UseGuards(JwtAuthGuard) it’s a good time to comment that line, otherwise you will not be able to test code unless you add a new user directly on PostgreSQL database.

Test Endpoints

Let’s fire up Postman (or similar app) and test our code:

Create new user

You can see that our user was successfully created, you can check that on our database table:

Inserted row

Now let’s try our login endpoint:

Login request

JWT Passport Strategy

Once our endpoints are done we need to code our passport. On jwt-auth.guard.ts we need to do a simple configuration:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

After this we can import our keys on our strategy module, this module will validate the tokens sent to our application. To achieve this purpose we only need the public key, so, let’s code it:

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: `${process.env.PUBLIC_KEY.replace(/\\\\n/gm, '\\n')}`,
algorithms: ['RS256'],
});
}

async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}

And it’s pretty much that. Let’s uncomment the line UseGuards(JwtAuthGuard) and check what happens:

Unauthorised request after AuthGuard

The request it’s unauthorised, now, if add the token given by our login endpoint, we can access this endpoint. Note: This token only lasts for 60s.

Add token on postman request
Creating a new user

It’s working! Now our authentication is implemented!

Conclusions

The development using this framework is simple and intuitive and, in addition, its documentation is very well defined and allows easy learning. Implementing authentication it’s easy and quick to do.

Souce code

The code for the entire project is available on my github.

--

--

No responses yet