(NestJS-9)Authentication in NestJS with JWT: A Practical Implementation
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:
- Validates user credentials using bcrypt.
- Generates a JWT token if the credentials are valid.
- 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:
- Extracts the JWT from the
Authorization
header. - Verifies the token using the JWT service.
- 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:
- JWT Secret Key and Expire time: Confirm that the
JWT_SECRET
andJWT_EXPIRESIN
environment variable is set in your.env
file. - Guard Configuration: Verify that the
AuthGuard
is applied globally in yourAppModule
or to specific routes.
Step 2: Test Public Routes
Routes marked with the @SkipAuthGuard
decorator should bypass the AuthGuard
.
Test Process
- Use a tool like Postman to send a request to a route marked with
@SkipAuthGuard()
. - 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
or403 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
- Send a request to a protected route, such as GET
/user
, without a token. Expected Response:401 Unauthorized
. - Obtain a valid JWT token by logging in (from the
/auth/login
route). - 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
- Modify the token or use an expired token.
- 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 🚀