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
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
}
}
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
populate it with the following content:
import { IsNotEmpty, IsString } from 'class-validator';
export class GetUserDto {
@IsString()
@IsNotEmpty()
_id: string;
}
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);
}
}
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
populate it with the following content:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
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;
}
}
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 {}
Add cookie-parser Middleware
install cookie-parser run the following command on your terminal
npm install cookie-parser
npm install -D @types/cookie-parser
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();
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);
}
}
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)