DEV Community

Jayvee Ramos
Jayvee Ramos

Posted on

8. JWT Authentication with NestJS and Passport

Introduction:

In this tutorial, we'll guide you through the process of implementing JWT (JSON Web Token) authentication using NestJS and Passport. JWT authentication is a common method for securing web applications by providing a way to authenticate users and authorize access to specific resources.

By the end of this tutorial, you'll have a working JWT authentication setup for your NestJS application. We will cover installing the necessary dependencies, configuring JWT, and handling environment variables for different microservices.

Let's get started!


Step 1: Installing Dependencies

To begin, we'll install the required dependencies for JWT authentication and Passport. Open your project directory and stop your application if it's running. Then, run the following commands to install the necessary packages:

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

Enter fullscreen mode Exit fullscreen mode

These packages are essential for setting up JWT authentication and Passport for your NestJS application.


Step 2: Configuring JWT

Now that we have the dependencies in place, it's time to configure JWT for your application. Open your auth.module.ts file, and update the content with the following code

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 { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    UsersModule,
    LoggerModule,
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'), //JWT_SECRET not yet defined
        signOptions: {
          expiresIn: `${configService.get<string>('JWT_EXPIRATION')}s`, //JWT_EXPIRATION not yet defined
        },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

Enter fullscreen mode Exit fullscreen mode

In this code, we're setting up the JWT module to use your environment variables, such as JWT_SECRET and JWT_EXPIRATION, which should be securely stored.


Step 3: Managing Environment Variables

To manage environment variables more effectively for different microservices, we'll create separate .env files for each service. This enhances modularity and reduces configuration overlap.

Create a unique .env file for the auth service and move all related environment variables into it.

to create .env file in your auth module just run the following command on your terminal:

touch apps/auth/.env
Enter fullscreen mode Exit fullscreen mode

populate it with the following content:

JWT_SECRET=your_secret_key
JWT_EXPIRATION=3600
Enter fullscreen mode Exit fullscreen mode

Similarly, create a .env file for the reservations service, just run this command

touch apps/reservations/.env
Enter fullscreen mode Exit fullscreen mode

populate it with the following content:

MONGODB_URI=mongodb://mongo:27017/reservation
Enter fullscreen mode Exit fullscreen mode

Step 4: Update docker-compose.yml

since we created a .env file for both of our different services we can now tell our docker-compose in which environment each of our services will be using, just update your docker-compose.yml file with the following content

services:
  reservations:
    build:
      context: .
      dockerfile: apps/reservations/Dockerfile
      target: development
    command: npm run start:dev reservations
    env_file:
      - ./apps/reservations/.env
    ports:
      - '3000:3000'
    volumes:
      - .:/usr/src/app
  auth:
    build:
      context: .
      dockerfile: apps/auth/Dockerfile
      target: development
    command: npm run start:dev auth
    env_file:
      - apps/auth/.env
    ports:
      - '3001:3001'
    volumes:
      - .:/usr/src/app
  mongo:
    image: mongo

Enter fullscreen mode Exit fullscreen mode

Step 5: Refactoring Configuration

We will start by removing the config directory entirely from lib/common. As a result, we'll only have the database and logger modules left.
you can delete the config directory in your libs/common/src manually or you can also run the following command on your terminal:

rm -r libs/common/src/config
Enter fullscreen mode Exit fullscreen mode

This change may initially lead to some issues, as the database module won't find the config module anymore. However, this is part of the plan.

To fix this, remove the config module import in the database module and also from the database module itself. The database module will now depend on the service calling it to set up the config module. (Update your database module with the following code)

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ModelDefinition, MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRootAsync({
      useFactory: (configService: ConfigService) => ({
        uri: configService.get('MONGODB_URI'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class DatabaseModule {
  static forFeature(models: ModelDefinition[]) {
    return MongooseModule.forFeature(models);
  }
}

Enter fullscreen mode Exit fullscreen mode

Now, let's set up the config module directly within each individual microservice. This way, each service can configure the config module specifically for its needs. This approach is more flexible and manageable than our previous setup.

also, remove the config module import on your libs/common/src/index.ts


Step 6: Setting Up the config Module for reservations

In each microservice, we'll import the config module from @nestjs/config and register it as global so that the config module is available to anyone who needs it in that service.

Additionally, we can define a validation schema for the required environment variables. For example, we can use the Joi library to define the schema.
update your reservations.module.ts with the following content:

import { Module } from '@nestjs/common';
import { ReservationsService } from './reservations.service';
import { ReservationsController } from './reservations.controller';
import { DatabaseModule, LoggerModule } from '@app/common';
import { ReservationsRepository } from './reservations.repository';
import {
  ReservationDocument,
  ReservationSchema,
} from './entities/reservation.entity';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';

@Module({
  imports: [
    DatabaseModule,
    DatabaseModule.forFeature([
      { name: ReservationDocument.name, schema: ReservationSchema },
    ]),
    LoggerModule,
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        MONGODB_URI: Joi.string().required(),
        PORT: Joi.number().required(), //we wil setup this env later
      }),
    }),
  ],
  controllers: [ReservationsController],
  providers: [ReservationsService, ReservationsRepository],
})
export class ReservationsModule {}

Enter fullscreen mode Exit fullscreen mode

So now we have this config set up for reservations and you can see
and then we'll do the same thing in the auth.module.ts.


Step 7: Setting Up the config Module for auth

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';

@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],
})
export class AuthModule {}


Enter fullscreen mode Exit fullscreen mode

Restart your containers to apply the changes, and run the following command on your terminal:

docker-compose up
Enter fullscreen mode Exit fullscreen mode

you should see no error in your logs


Step 8: Dynamic Port Allocation

Lastly, let's make our microservices even more dynamic by allocating ports dynamically. In the** main.ts file of each service**, you can retrieve the port value from the config service. This allows you to avoid hardcoding port numbers, which is essential when you plan to deploy your applications.

update your apps/auth/src/main.ts file with the following code:

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';

async function bootstrap() {
  const app = await NestFactory.create(AuthModule);
  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

update your apps/reservations/src/main.ts file with the following code:

import { NestFactory } from '@nestjs/core';
import { ReservationsModule } from './reservations/reservations.module';
import { ValidationPipe } from '@nestjs/common';
import { Logger } from 'nestjs-pino';
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
  const app = await NestFactory.create(ReservationsModule);
  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

update your apps/reservations/.env file with the following code:

MONGODB_URI=mongodb://mongo:27017/reservation
PORT=3000
Enter fullscreen mode Exit fullscreen mode

update your apps/auth/.env file with the following code:

JWT_SECRET=your_secret_key
JWT_EXPIRATION=3600
PORT=3001
Enter fullscreen mode Exit fullscreen mode

Congratulations! You've successfully implemented JWT authentication for your NestJS application using Passport and improved your environment variable management for different microservices.

Top comments (0)