Authentication with JsonWebToken in NestJS

Introduction

JWT token authentication has become a de facto standard when it comes to protecting the most sensitive endpoints of our application: those that create, modify or delete resources from our server and other queries that return sensitive information that should only be accessible by the specific user.

In fact, here we are going to see how to send the user id through a token, something much safer than doing it by parameter in the URL. Imagine an endpoint that returns judicial data, and simply by changing the id by parameter and making the call again, it returns the judicial data, which obviously has to be confidential, from any person you don’t know at all, well that has happened… .

Let’s start doing it

In NodeJS, Passport is usually used to configure the authentication methods, so NestJS already offers us a direct integration with this library, simply by installing the following dependencies.

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

In addition to Passport, we also install the bcrypt dependency that will allow us to encode the password in the database, so that no one, even people with permissions to see the user credential tables, can know your password.

Continuing with the series of tutorials we already have in our project a file src/users/user.entity.ts that defines our user entity and that we are going to use to associate the password that we will use to verify the entity of the person in the process of login.

For this reason we need to add the password column, and two methods: one that will be executed before inserting the entity and that will be in charge of encoding the password and another that we will use later to validate that the user’s password when logging in is valid, thanks to the bcrypt library.

The UserEntity entity would look like this:

import * as bcrypt from 'bcrypt';
import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
readonly userId: string;
  @Column({
unique: true,
})
readonly name: string;
  @Column({ type: 'varchar', length: 70, nullable: true })
password: string;
  @BeforeInsert()
async hashPassword() {
const salt = await bcrypt.genSalt();
this.password = await bcrypt.hash(this.password, salt);
}
  async validatePassword(password: string): Promise<boolean> {
return await bcrypt.compareSync(password, this.password);
}
  constructor(userId: string, name: string, pass: string) {
this.userId = userId;
this.name = name;
this.password = pass;
}
}

You will already know from the tutorial series that we are using TypeORM migrations, so now is the time to generate a new migration file by running the command:

npm run typeorm:migrate add-auth

Now we go to the UsersRepository service to add a new method that allows me to retrieve a user from their name.

getUserByName(name: string): Promise<UserEntity> {
return this.usersRepository.findOne({ name });
}

This method is going to be called by the service layer, so within the UsersService service we are going to create a new method that makes the call to the repository.

async getUserByName(name: string): Promise<UserEntity> {
return await this.usersRepository.getUserByName(name);
}

This is a good time to see that everything is correct, that the migration is applied correctly and that all our tests are still passing, so we run:

npm run verify

We will have to see that everything is still correct, although we now have a little lower coverage due to having new code that has not been tested yet.

Once we have everything we need to save our users’ credentials, it’s time to create a new module called “auth”:

npx nest generate module auth

Within the new module we are going to create the file auth.service, which is going to implement two methods: one “validateUser” that is going to be in charge of validating the user’s credentials, that is, that the username exists in the database and that your password matches the one received at the login endpoint; and another method “generateAccessToken” that will be in charge of generating the accessToken with the information that we want to save in the JWT token payload, for our use case, we make a query to the database, to retrieve the user id and save it in the payload of the JWT token, which we remember is clear, so there is no need to store sensitive information such as ID, account numbers, passwords, etc…

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';
import { JWTPayload } from './jwt.payload';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
  async validateUser(username: string, pass: string): Promise<boolean> {
const user = await this.usersService.getUserByName(username);
return await user.validatePassword(pass);
}
  async generateAccessToken(name: string) {
const user = await this.usersService.getUserByName(name);
const payload: JWTPayload = { userId: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}

When you work with TypeScript it is good practice to create interfaces / classes that save the type of the objects that we use. In this case, we are going to create the auth/jwt.payload.ts file with the following content:

export interface JWTPayload {
userId: string;
}

We still create a class (to be able to use the Swagger annotations for the API documentation) to model the data that we are going to receive in the body of the POST method that we will implement next to grant or not access to the user through a token. applicant.

import { ApiProperty } from '@nestjs/swagger';
export class LoginDTO {
@ApiProperty()
name: string;
  @ApiProperty()
pass: string;
}

And then the controller that will be in charge of receiving the login requests from the users (auth/auth.controller.ts), validating the credentials and throwing an exception in case the user is not found or generating the token if the user is found and the credentials are valid.

import { Body, Controller, Post, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDTO } from './login.dto';
@Controller('login')
export class AuthController {
constructor(private authService: AuthService) {}
  @Post()
async login(@Body() loginDTO: LoginDTO): Promise<{ access_token: string }> {
const { name, pass } = loginDTO;
const valid = await this.authService.validateUser(name, pass);
if (!valid) {
throw new UnauthorizedException();
}
return await this.authService.generateAccessToken(name);
}
}

In order to make use of the JwtService service to generate the JWT tokens, we have to import the JwtModule module inside the AuthModule and set the expiration period of the token and the secret word for its encryption. We will save this secret word in the .env file as an environment variable, and load it by importing the ConfigModule module. In addition, we have to register all the implemented services as providers, leaving the code in this form:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from 'src/users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
imports: [
ConfigModule.forRoot(),
UsersModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '60s' },
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}

With this we already have an endpoint that grants us tokens to valid users, where we store their id to avoid having to pass it by URL. Remember that the token is signed and cannot be tampered with unless you expose the secret word.

Now we get to the second part of the goal which is to protect certain endpoints so that they can only be executed if the client has a valid JWT token.

For that Passport defines different strategies where one of the most common is JWT. We create the auth/jwt.strategy.ts file with the following content:

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserDTO } from 'src/users/user.dto';
import { UsersService } from 'src/users/users.service';
import { JWTPayload } from './jwt.payload';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
  async validate(payload: JWTPayload): Promise<UserDTO> {
const user = await this.usersService.getUserById(payload.userId);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}

In this file we define several things, on the one hand that the token is going to be received through the HTTP header called Authorization, that we are not going to fire an exception when the token is expired and we set the same environment variable for the secret.

It also implements the validate method which, through the JWT token payload, will determine whether or not the person is authorized, and if so, establish the information that we can later retrieve in the request.

To register this strategy we have to import the PassportModule module and the JwtStrategy provider in auth.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from './../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
ConfigModule.forRoot(),
UsersModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '60d' },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

It is time to establish security in the endpoints, for this we make use of the NestJS guard concept that allows, through the @UseGuards decorator, to establish n guards that must be fulfilled so that the endpoint can be executed.

In our example case of the user crud, we are going to establish the guard in the GET, PUT and DELETE by Id methods, leaving the controller in this way:

import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
import { ValidUserIdPipe } from './../pipes/valid-user-id.pipe';
import { UserDTO } from './user.dto';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
  @Get()
async getAllUsers(): Promise<UserDTO[]> {
return await this.usersService.getAllUsers();
}
  @Get(':id')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
async getUserById(@Param('id') id: string): Promise<UserDTO> {
return await this.usersService.getUserById(id);
}
  @Post()
async newUser(@Body() user: UserDTO): Promise<UserDTO> {
return await this.usersService.newUser(user);
}
  @Put(':id')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
async updateUser(
@Param('id', ValidUserIdPipe) id: string,
@Body() user: UserDTO
): Promise<UserDTO> {
return await this.usersService.updateUser(id, user);
}
  @UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
@Delete(':id')
async deleteUser(@Param('id') id: string): Promise<void> {
return await this.usersService.deleteUser(id);
}
}

This way endpoints can only be executed when they are called with a valid token. Now we are going to avoid that in the endpoint of retrieving the data by id we have to pass the id by parameter, retrieving it from the token. For what, instead of retrieving it from the URL, we are going to do it from the request where the JwtStrategy validate method has left the returned data in a property that we can consult called “user”.

So the controller would look like this:

@Get('/me')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
async getUserById(@Request() req: any): Promise<UserDTO> {
const { id } = req.user;
return await this.usersService.getUserById(id);
}

Another way to retrieve the user information, via the token, is to implement your own decorator in NestJS. We create the file “auth/auth.decorator.ts” with the following content:

import {
createParamDecorator,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { JWTPayload } from './jwt.payload';
export const Auth = createParamDecorator(
(data: unknown, ctx: ExecutionContext): Partial<JWTPayload> => {
try {
const request = ctx.switchToHttp().getRequest();
return request.user;
} catch (error) {
throw new ForbiddenException();
}
}
);

So now we can use it on any of the protected endpoints to retrieve the information stored by the JWT strategy in the request. This would be the example for the case of returning a user.

@Get('/me')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
async getUserById(@Auth() { id }: UserDTO): Promise<UserDTO> {
return await this.usersService.getUserById(id);
}

Now we can find the case in which we want a person with an administrator role to be able to do any type of operation with any user, such as updating their data or deleting the user.