Building Secure JWT Authentication with NestJS Guards and Decorators
Introduction
Authentication is the backbone of any secure web application. While basic JWT implementation might seem straightforward, building a production-ready authentication system requires careful consideration of security best practices, token management, and proper error handling. In this guide, we'll build a comprehensive JWT authentication system in NestJS using guards, decorators, and following security best practices.
Setting Up the Foundation
First, let's install the necessary dependencies:
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs
npm install -D @types/passport-jwt @types/bcryptjsCreate a robust JWT service that handles token generation and validation:
// auth/jwt.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class CustomJwtService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
generateTokens(userId: string, email: string) {
const payload = { sub: userId, email };
const accessToken = this.jwtService.sign(payload, {
expiresIn: '15m',
secret: this.configService.get('JWT_ACCESS_SECRET'),
});
const refreshToken = this.jwtService.sign(payload, {
expiresIn: '7d',
secret: this.configService.get('JWT_REFRESH_SECRET'),
});
return { accessToken, refreshToken };
}
verifyAccessToken(token: string) {
return this.jwtService.verify(token, {
secret: this.configService.get('JWT_ACCESS_SECRET'),
});
}
verifyRefreshToken(token: string) {
return this.jwtService.verify(token, {
secret: this.configService.get('JWT_REFRESH_SECRET'),
});
}
}Creating a Secure Authentication Guard
Build a custom JWT guard that handles token extraction and validation with proper error handling:
// auth/jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { CustomJwtService } from './jwt.service';
import { UserService } from '../user/user.service';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: CustomJwtService,
private userService: UserService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): boolean {
const isPublic = this.reflector.getAllAndOverride('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('Access token is required');
}
try {
const payload = this.jwtService.verifyAccessToken(token);
const user = await this.userService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('Invalid user or account disabled');
}
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid or expired token');
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
} Building Useful Decorators
Create custom decorators for cleaner code and better developer experience:
// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Public = () => SetMetadata('isPublic', true);
// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);Implementing the Authentication Controller
Create a comprehensive auth controller with login, refresh, and logout functionality:
// auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { Public } from '../decorators/public.decorator';
import { CurrentUser } from '../decorators/current-user.decorator';
@Controller('auth')
@UseGuards(JwtAuthGuard)
export class AuthController {
constructor(private authService: AuthService) {}
@Public()
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@Public()
@Post('refresh')
@HttpCode(HttpStatus.OK)
async refresh(@Body() refreshDto: RefreshTokenDto) {
return this.authService.refreshTokens(refreshDto.refreshToken);
}
@Post('logout')
@HttpCode(HttpStatus.OK)
async logout(@CurrentUser() user: any, @Body() logoutDto: LogoutDto) {
return this.authService.logout(user.id, logoutDto.refreshToken);
}
@Post('profile')
getProfile(@CurrentUser() user: any) {
return { user: { id: user.id, email: user.email, role: user.role } };
}
}Security Best Practices
Implement additional security measures:
1. Token Blacklisting
Store invalidated refresh tokens in Redis to prevent reuse:
// auth/auth.service.ts
async logout(userId: string, refreshToken: string) {
// Verify the refresh token
const payload = this.jwtService.verifyRefreshToken(refreshToken);
// Add to blacklist
await this.redisService.set(
`blacklist:${refreshToken}`,
'true',
'EX',
7 * 24 * 60 * 60 // 7 days
);
return { message: 'Logged out successfully' };
}2. Rate Limiting
Implement rate limiting for authentication endpoints:
// Install: npm install @nestjs/throttler
@Controller('auth')
@Throttle(5, 60) // 5 requests per minute
export class AuthController {
@Public()
@Post('login')
@Throttle(3, 60) // Stricter limit for login
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
}Error Handling and Logging
Implement comprehensive error handling:
// Create a custom exception filter for auth errors
@Catch(UnauthorizedException)
export class AuthExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AuthExceptionFilter.name);
catch(exception: UnauthorizedException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
this.logger.warn(`Unauthorized access attempt: ${request.ip} - ${request.url}`);
response.status(401).json({
statusCode: 401,
timestamp: new Date().toISOString(),
path: request.url,
message: 'Authentication failed',
});
}
}Conclusion
This implementation provides a solid foundation for JWT authentication in NestJS with proper security measures. Key features include separate access and refresh tokens, token blacklisting, rate limiting, comprehensive error handling, and clean decorator usage. Remember to regularly rotate JWT secrets, implement proper HTTPS in production, and monitor authentication logs for suspicious activity. The modular approach makes it easy to extend with additional features like multi-factor authentication or OAuth integration.
Related Posts
Building Scalable GraphQL APIs with NestJS: A Practical Guide
Learn to create powerful, type-safe GraphQL APIs using NestJS with practical examples and best practices for scalable applications.
Building Production-Ready REST APIs with FastAPI and Pydantic
Learn how to build robust, type-safe REST APIs using FastAPI and Pydantic with proper validation, documentation, and error handling.
Implementing GraphQL with NestJS: A Complete Guide for Modern API Development
Learn how to build scalable GraphQL APIs with NestJS using decorators, resolvers, and type-safe schemas for modern backend development.