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
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 {}
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
populate it with the following content:
JWT_SECRET=your_secret_key
JWT_EXPIRATION=3600
Similarly, create a .env file for the reservations service, just run this command
touch apps/reservations/.env
populate it with the following content:
MONGODB_URI=mongodb://mongo:27017/reservation
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
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
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);
}
}
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 {}
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 {}
Restart your containers to apply the changes, and run the following command on your terminal:
docker-compose up
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();
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();
update your apps/reservations/.env file with the following code:
MONGODB_URI=mongodb://mongo:27017/reservation
PORT=3000
update your apps/auth/.env file with the following code:
JWT_SECRET=your_secret_key
JWT_EXPIRATION=3600
PORT=3001
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)