Authentication in NestJS — PostgreSQL and JWT
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.
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.
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:
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:
You can see that our user was successfully created, you can check that on our database table:
Now let’s try our login endpoint:
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:
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.
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.