DEV Community

Jayvee Ramos
Jayvee Ramos

Posted on

10. Implementing JWT Authentication

Introduction

In our previous tutorial, we set up local authentication in a Nest.js application to handle user login and authentication. Now, we'll take it a step further and implement JWT (JSON Web Token) authentication to secure routes and endpoints in our application.

Creating the JWT Strategy

Create JWT Strategy: First, we need to create a JWT strategy that validates JWT tokens. Inside your auth/strategies folder, create a new TypeScript file, jwt.strategy.ts, and define the JWT strategy:
run the command to create jwt.strategy.ts

touch apps/auth/src/strategies/jwt.strategy.ts
Enter fullscreen mode Exit fullscreen mode

populate it with the following content

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { UsersService } from '../users/users.service';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import { Tokenpayload } from '../interface/token-payload.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private readonly configService: ConfigService,
    private readonly userService: UsersService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        (request: Request) => request.cookies.Authentication, // JWT in a cookie
      ]),
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate({ userId }: Tokenpayload) {
    return this.userService.getUser({ _id: userId });  //We will define 'getUser' in the user service later; for now, let's just declare it
  }
}

Enter fullscreen mode Exit fullscreen mode

Creating DTO for getUser

To create get-user.dto.ts inside our user/dto folder run this command on your terminal:

touch apps/auth/src/users/dto/get-user.dto.ts
Enter fullscreen mode Exit fullscreen mode

populate it with the following content:

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

export class GetUserDto {
  @IsString()
  @IsNotEmpty()
  _id: string;
}

Enter fullscreen mode Exit fullscreen mode

In this DTO, we use the @IsEmail decorator to validate that the email field has a valid email format. This DTO is intended for retrieving a user by their email, and it enforces email format validation.


Implementing getUser method in user service

open your user.service.ts and update the content with the following code:

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { CreateUserDTO } from './dto/create-user.dto';
import * as bcryptjs from 'bcryptjs';
import { GetUserDto } from './dto/get-user.dto';
@Injectable()
export class UsersService {
  constructor(private readonly userRepository: UserRepository) {}

  async create(createUserDto: CreateUserDTO) {
    return this.userRepository.create({
      ...createUserDto,
      password: await bcryptjs.hash(createUserDto.password, 10),
    });
  }

  async verifyUser(email: string, password: string) {
    const user = await this.userRepository.findOne({ email });
    const passwordIsValid = await bcryptjs.compare(password, user.password);
    if (!passwordIsValid) {
      throw new UnauthorizedException('Invalid credentials');
    }
    return user;
  }

  async getUser(getUserDto: GetUserDto) {
    return this.userRepository.findOne(getUserDto);
  }
}

Enter fullscreen mode Exit fullscreen mode

we just added a getUser method


Creating the JWT Auth Guard

Create JWT Auth Guard: Similar to our local authentication guard, we need an auth guard for JWT. Create a jwt-auth.guard.ts file in your guards folder to do this run this command on your terminal:

touch apps/auth/src/guards/jwt-auth.guard.ts
Enter fullscreen mode Exit fullscreen mode

populate it with the following content:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Enter fullscreen mode Exit fullscreen mode

Using JWT Authentication

Use JWT Auth Guard: You can use the JwtAuthGuard in your controller methods or routes to protect them.

Apply JWT Authentication: In your controllers, you can apply JWT authentication using the @UseGuards decorator. to protect routes: For example, to protect a route that returns the current update your user.controller.ts content with the following code:

import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDTO } from './dto/create-user.dto';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { CurrentUser } from '../current-user.decorator';
import { UserDocument } from './models/user.schema';

@Controller('users')
export class UsersController {
  constructor(private readonly userService: UsersService) {}

  @Post()
  async createUser(@Body() createUserDto: CreateUserDTO) {
    return this.userService.create(createUserDto);
  }

  @Get()
  @UseGuards(JwtAuthGuard)
  getCurrentUser(@CurrentUser() user: UserDocument) {
    return user;
  }
}

Enter fullscreen mode Exit fullscreen mode

In the auth module, add the jwt strategy as a provider:
update your auth.module.ts with the following content

import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersModule } from './users/users.module';
import { LoggerModule } from '@app/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as Joi from 'joi';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';

@Module({
  imports: [
    UsersModule,
    LoggerModule,
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        JWT_SECRET: Joi.string().required(),
        JWT_EXPIRATION: Joi.string().required(),
        PORT: Joi.number().required(), //we wil setup this env later
      }),
    }),
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: {
          expiresIn: `${configService.get<string>('JWT_EXPIRATION')}s`,
        },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule {}

Enter fullscreen mode Exit fullscreen mode

Add cookie-parser Middleware

install cookie-parser run the following command on your terminal

npm install cookie-parser
npm install -D @types/cookie-parser
Enter fullscreen mode Exit fullscreen mode

open your auth/src/main.ts file and update it with the following content to use cookie-parser as middleware

import { NestFactory } from '@nestjs/core';
import { AuthModule } from './auth.module';
import { ValidationPipe } from '@nestjs/common';
import { Logger } from 'nestjs-pino';
import { ConfigService } from '@nestjs/config';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AuthModule);
  app.use(cookieParser());
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
    }),
  );
  app.useLogger(app.get(Logger));
  const configService = app.get(ConfigService);
  await app.listen(configService.get('PORT'));
}
bootstrap();

Enter fullscreen mode Exit fullscreen mode

Validating Unique Email during User Creation

Ensure Unique Email: To prevent the creation of multiple users with the same email, you can add validation during user creation. Modify your user service to validate email uniqueness before creating a new user: update your user.service.ts with the following content:

import {
  Injectable,
  UnauthorizedException,
  UnprocessableEntityException,
} from '@nestjs/common';
import { UserRepository } from './user.repository';
import { CreateUserDTO } from './dto/create-user.dto';
import * as bcryptjs from 'bcryptjs';
import { GetUserDto } from './dto/get-user.dto';
@Injectable()
export class UsersService {
  constructor(private readonly userRepository: UserRepository) {}

  async create(createUserDto: CreateUserDTO) {
    await this.validateCreateUserDto(createUserDto);
    return this.userRepository.create({
      ...createUserDto,
      password: await bcryptjs.hash(createUserDto.password, 10),
    });
  }

  private async validateCreateUserDto(createUserDto: CreateUserDTO) {
    try {
      await this.userRepository.findOne({ email: createUserDto.email });
    } catch (error) {
      return;
    }
    throw new UnprocessableEntityException('Email already exists');
  }

  async verifyUser(email: string, password: string) {
    const user = await this.userRepository.findOne({ email });
    const passwordIsValid = await bcryptjs.compare(password, user.password);
    if (!passwordIsValid) {
      throw new UnauthorizedException('Invalid credentials');
    }
    return user;
  }

  async getUser(getUserDto: GetUserDto) {
    return this.userRepository.findOne(getUserDto);
  }
}

Enter fullscreen mode Exit fullscreen mode

We have just added a 'validateCreateUserDto' function to verify the existence of an email in our database, ensuring email uniqueness.


With these steps, you've implemented JWT authentication in your Nest.js application, ensuring secure access to protected routes. Additionally, you've added email uniqueness validation during user creation to maintain data integrity in your user database.

In the next tutorial, we'll explore more advanced authentication features and user management.

Stay tuned for the next part of our Nest.js authentication series!

Top comments (0)