(NestJS-9)Authentication in NestJS with JWT: A Practical Implementation

Bhargava Chary
7 min readNov 2, 2024

--

Authentication is essential for modern web applications to ensure that only authorized users can access resources. In this article, we’ll walk you through setting up authentication and authorization in NestJS using JWT (JSON Web Token) for session management and cookies to securely store the token.

What You’ll Need

Before we begin, make sure you have the following ready:

  • NestJS Project Setup: If you don’t have a project, check out the Getting Started with NestJS article.
  • TypeORM and Database: Installed and set up. If not, follow the TypeORM Configuration article.
  • CRUD Operations: Familiarity with basic CRUD operations. If needed, refer to the NestJS CRUD Operations using TypeORM article.
  • Encrypt Password: Familiarity with using bcrypt. If needed, refer to the Use bcrypt to encrypt password article.

Why Do We Need Authentication and Authorization?

When you log into apps like Gmail or Instagram, the system verifies your identity — this is authentication. After authentication, you are authorized to access only specific resources based on your role.

  • Authentication: Confirms your identity (Who are you?).
  • Authorization: Determines your permissions (What can you access?).

Even if someone gains access to an account, they cannot interact with restricted resources without the right authorization.

Install the Required Packages

Let’s install the dependencies needed for JWT and cookie handling:

$ npm install @nestjs/jwt cookie-parser
$ npm i -D @types/cookie-parser
  • @nestjs/jwt: Manages JWT generation and verification.
  • cookie-parser: Handles cookies securely in your application.

Configuring Environment Variables

Add the following variables to your .env or .env.development or .env.production depending on your setup:

JWT_SECRET = 'jwt-secret'
JWT_EXPIRESIN = '5h'
  • JWT_SECRET: A unique and secure key for signing JWTs. You can generate a strong key using:

To generate a strong secret key, run this command:

 python -c 'import secrets; print(secrets.token_hex())'
  • JWT_EXPIRESIN: Specifies how long the JWT token remains valid.

Creating the Authentication Module

Generate the required module, controller, and service:

$ nest g module auth
$ nest g controller auth
$ nest g service auth

Building the Authentication Controller

The AuthController will manage the login process. If the login is successful, a JWT token will be sent to the user in a secure cookie.

Defining DTO for Login

Define the structure for the incoming request:

// src/auth/dto/auth.dto.ts

import { IsEmail, IsNotEmpty, IsString } from "class-validator";

export class LoginDto {
@IsEmail()
@IsNotEmpty()
email:string

@IsString()
@IsNotEmpty()
password:string
}

AuthController

Handle login controller logic

import { Body, Controller, Post, Response } from '@nestjs/common';
import { LoginDto } from './dto/auth.dto';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('login')
async login(
@Body() loginDto: LoginDto,
@Response({ passthrough: true }) res,
) {
return await this.authService.login(loginDto, res);
}
}

Explanation of this controller:

  • Accepts login requests with user credentials.
  • Delegates validation to the service.
  • Sends a JWT token in an HTTP-only cookie upon success.

Handling Login Logic

Finding User by Email

Update the UserService to locate a user by email:

// src/user/user.service.ts
/*imports*/

@Injectable()
export class UserService {
/* other methods */
async findByEmail(email: string) {
return await this.userRepository.findOne({ where: { email: email }, select: { user_id: true, name: true, password: true, role: true } })
}
}

Note: Ensure that all the selected options exist in the UserEntity. If any required columns are missing, make sure to add them to the entity definition. Dont forgot to sync the database while starting application.

Add a Role Column to the User Entity

Add role column in userEntity

// src/user/entities/user.entity.ts

export enum RoleEnum { ADMIN="ADMIN", SELLER="SELLER", CONSUMER="CONSUMER" }

@Entity()
export class User {

@Column({
type:'enum',
enum: RoleEnum,
default: RoleEnum.CONSUMER
})
role: RoleEnum;

}

AuthService for Validation and Token Generation

// src/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { User } from 'src/user/entities/user.entity';
import * as bcrypt from 'bcrypt';
import { UserService } from 'src/user/user.service';
import { JwtService } from '@nestjs/jwt';
import { LoginDto } from './dto/auth.dto';

@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private jwtService: JwtService,
) { }

async validateUser(email: string, password: string): Promise<{ status: boolean, payload?: User, message?: string } | null> {
const user = await this.userService.findByEmail(email);
if (user && await bcrypt.compare(password, user.password)) {
return { "status": true, "payload": user }; // Password matches
} else {
return { "status": false, "message": "Invalid email or password!" };
}
}

async login(loginDto: LoginDto, res) {
const user = await this.validateUser(loginDto.email, loginDto.password);
if (!user.status) {
res.status(401).send(user.message);
return;
}
const payload = { sub: user.payload.user_id, name: user.payload.name, role: user.payload.role };
const access_token = await this.jwtService.signAsync(payload, {
expiresIn: process.env.JWT_EXPIRESIN,
secret: process.env.JWT_SECRET,
});
res.cookie('access_token', access_token, {
httpOnly: true,
secure: true,
});
res.send('Login Successful!');
}
}

Explanation of the service:

  1. Validates user credentials using bcrypt.
  2. Generates a JWT token if the credentials are valid.
  3. Signs the JWT with a secret and an expiration time.

Configuring Middleware

Enable cookie parsing in the main application file:

//main.ts
// other imports
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
// other configurations
}
bootstrap();

Setting Up AuthModule

Tie the components together:

// auth.module.ts

import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { Profile } from 'src/user/entities/profile.entity';
import { User } from 'src/user/entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from 'src/user/user.service';

@Module({
imports: [
JwtModule.register({}),
TypeOrmModule.forFeature([User, Profile]),
],
controllers: [AuthController],
providers: [
AuthService,
UserService
]
})
export class AuthModule { }

Implementing Authentication Guard

Define the AuthGuard to verify the JWT

// src/auth/jwtauth.guard.ts

import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const token = request.cookies?.access_token;

if (!token) {
throw new UnauthorizedException();
}

try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
request["user"] = payload;
} catch {
throw new UnauthorizedException();
}

return true;
}
}

Explanation of this guard:

  1. Extracts the JWT from the Authorization header.
  2. Verifies the token using the JWT service.
  3. Attaches the user payload to the request object.

Protecting Routes

Here’s an example route that uses the AuthGuard to allow only authenticated users. This is only an example testing purpose dont apply guard to this route.

import { Controller, Get, Req, UseGuards, Ip } from '@nestjs/common';
import { AuthGuard } from './auth/jwtauth.guard';
import { AppService } from './app.service';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@UseGuards(AuthGuard)
@Get()
getHello(@Req() req, @Ip() ip: string): string {
console.log(`GetHello call from IP: ${ip}, User: ${req.user}`);
return this.appService.getHello();
}
}

This route:

  • Only authenticated users with a valid JWT, will access the protected route.

Enable guard globally

To enable authguard globally, we need to register AuthGuard as global guard like below in Authmodule .


providers: [
// other providers
{
provide: APP_GUARD,
useClass: AuthGuard,
},
]

By above action the AuthGuard is bind to all routes.

Skipping the Authentication Guard for Specific Routes

Sometimes, you might have public routes like that should bypass the authentication guard, such as login, registration, or health-check endpoints. NestJS provides decorators to handle this scenario easily.

Create the SkipAuthGuard Decorator

Add the following in a file auth/skipauth.guard.ts:

// auth/skipauth.guard.ts
import { SetMetadata } from '@nestjs/common';
export const SkipAuthGuard = () => SetMetadata('skipAuthGuard', true);

Update the Authentication Guard to Handle Skipped Routes

In auth/jwtauth.guard.ts, add logic to check for the SkipAuthGuard metadata:

@Injectable()  
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService, private reflector: Reflector) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const skipGuard = this.reflector.get<boolean>('skipAuthGuard', context.getHandler());
if (skipGuard) {
return true;
}
//authentication functionality code
}

Applying SkipAuthGuard to Routes

Use the @SkipAuthGuard() decorator on routes that should bypass authentication. For example:

@Post('login')
@SkipAuthGuard()
async login(
@Body() loginDto: LoginDto,
@Response({ passthrough: true }) res,
) {
return await this.authService.login(loginDto, res);
}

Now you can authenticate request from using routes.

Testing the Implementation

Step 1: Verify Setup

Before testing, ensure the following:

  1. JWT Secret Key and Expire time: Confirm that the JWT_SECRET and JWT_EXPIRESIN environment variable is set in your .env file.
  2. Guard Configuration: Verify that the AuthGuard is applied globally in your AppModule or to specific routes.

Step 2: Test Public Routes

Routes marked with the @SkipAuthGuard decorator should bypass the AuthGuard.

Test Process

  1. Use a tool like Postman to send a request to a route marked with @SkipAuthGuard().
  2. Example: For the POST /auth/login endpoint:

Request:

{   
"username": "testuser",
"password": "password123"
}

Expected Response:

  • If valid credentials are provided, you should receive a response with a JWT token in a cookie.
  • If credentials are invalid, an error response like 401 Unauthorized or 403 Forbidden.

Validation Points

  • Ensure the route is accessible without a token.
  • Verify the response does not throw an UnauthorizedException.

Step 3: Test Protected Routes

Routes without the @SkipAuthGuard() decorator should require a valid JWT token for access.

Test Process

  1. Send a request to a protected route, such as GET /user, without a token. Expected Response: 401 Unauthorized.
  2. Obtain a valid JWT token by logging in (from the /auth/login route).
  3. Include the token as a cookie in the request header when accessing the protected route.
  • Example: Use Postman’s cookie manager or set the Cookie header manually: Cookie: access_token=your-valid-jwt-token
  • Expected Response: Successfully access the data, with a 200 OK status and the expected response body.

Validation Points

  • Confirm the route is inaccessible without a valid token.
  • Verify that a valid token grants access.

Step 4: Test Behavior for Expired or Invalid Tokens

Test Process

  1. Modify the token or use an expired token.
  2. Send a request to the protected route with the invalid or expired token.
  • Expected Response: 401 Unauthorized.

Validation Points

  • Ensure the application identifies invalid tokens.
  • Verify appropriate error messages (e.g., “Token expired” or “Invalid token”).

Wrapping Up

In this guide, we’ve covered:

  • Validated users with bcrypt.
  • Used JWT for secure authentication.
  • Secured routes using guards.

With these steps, your backend is now secure and ready for real-world use.

GitHub Repository:
You can find the complete code for this implementation on GitHub: bhargavachary123

Next Steps

Stay tuned for the next article on authorization user by role!

If you found this article useful, please consider leaving a clap (👏) and a comment ( / ).

Stay tuned for more articles covering advanced topics in NestJS development! Happy coding 🚀

--

--

Bhargava Chary
Bhargava Chary

No responses yet