Implementing Custom JWT Authentication in NestJS: A Production-Ready Guide
Introduction
Authentication is the backbone of most modern applications, and NestJS provides excellent tools for implementing secure JWT-based authentication. In this comprehensive guide, we'll build a production-ready JWT authentication system with refresh tokens, role-based access control, and proper security measures.
Setting Up the Foundation
First, let's install the required dependencies:
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install -D @types/passport-jwt @types/bcryptCreate the authentication module structure:
nest generate module auth
nest generate service auth
nest generate controller authCreating the User Entity and Service
Let's start with a User entity that includes role-based access:
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator'
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
role: UserRole;
@Column({ nullable: true })
refreshToken: string;
@Column({ default: true })
isActive: boolean;
}Implementing the Authentication Service
Now let's create a robust authentication service with proper password hashing and token management:
// auth.service.ts
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './user.entity';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private userRepository: Repository,
private jwtService: JwtService,
) {}
async register(email: string, password: string) {
const existingUser = await this.userRepository.findOne({ where: { email } });
if (existingUser) {
throw new ConflictException('User already exists');
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = this.userRepository.create({
email,
password: hashedPassword,
});
await this.userRepository.save(user);
return this.generateTokens(user);
}
async login(email: string, password: string) {
const user = await this.userRepository.findOne({ where: { email } });
if (!user || !user.isActive) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
return this.generateTokens(user);
}
async refreshTokens(userId: number, refreshToken: string) {
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user || !user.refreshToken) {
throw new UnauthorizedException('Access denied');
}
const isRefreshTokenValid = await bcrypt.compare(refreshToken, user.refreshToken);
if (!isRefreshTokenValid) {
throw new UnauthorizedException('Access denied');
}
return this.generateTokens(user);
}
private async generateTokens(user: User) {
const payload = { sub: user.id, email: user.email, role: user.role };
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
const hashedRefreshToken = await bcrypt.hash(refreshToken, 12);
await this.userRepository.update(user.id, { refreshToken: hashedRefreshToken });
return { accessToken, refreshToken };
}
async logout(userId: number) {
await this.userRepository.update(userId, { refreshToken: null });
}
} Creating JWT Strategy and Guards
Implement the JWT strategy for passport:
// jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(User)
private userRepository: Repository,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
const user = await this.userRepository.findOne({ where: { id: payload.sub } });
if (!user || !user.isActive) {
throw new UnauthorizedException();
}
return user;
}
} Create a role-based guard:
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from './user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
} Building the Authentication Controller
Create endpoints for authentication operations:
// auth.controller.ts
import { Controller, Post, Body, UseGuards, Get, Req } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
import { AuthService } from './auth.service';
import { UserRole } from './user.entity';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Body() body: { email: string; password: string }) {
return this.authService.register(body.email, body.password);
}
@Post('login')
async login(@Body() body: { email: string; password: string }) {
return this.authService.login(body.email, body.password);
}
@Post('refresh')
async refreshTokens(@Body() body: { refreshToken: string }, @Req() req) {
return this.authService.refreshTokens(req.user.id, body.refreshToken);
}
@UseGuards(JwtAuthGuard)
@Post('logout')
async logout(@Req() req) {
return this.authService.logout(req.user.id);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@Get('admin-only')
getAdminData() {
return { message: 'This is admin-only data' };
}
}Security Best Practices
Always implement these security measures in production:
- Environment Variables: Store JWT secrets and database credentials securely
- Rate Limiting: Implement rate limiting on authentication endpoints
- HTTPS Only: Ensure all authentication happens over HTTPS
- Token Rotation: Regularly rotate refresh tokens
- Password Policies: Enforce strong password requirements
- Account Lockout: Implement account lockout after failed attempts
Conclusion
This implementation provides a solid foundation for JWT authentication in NestJS applications. The combination of access and refresh tokens, role-based access control, and proper security measures ensures your authentication system is both secure and scalable. Remember to regularly update dependencies and review security practices as your application grows.
Related Posts
Implementing Clean Architecture with NestJS: Building Scalable Enterprise Applications
Learn how to implement Clean Architecture principles in NestJS applications for better maintainability and testability.
Building a Real-Time Chat Application with Socket.IO and Express
Learn to build a scalable real-time chat app using Socket.IO, Express, and modern JavaScript patterns with authentication and room management.
Building Production-Ready APIs with NestJS: A Complete Guide to Scalable Backend Architecture
Master NestJS fundamentals and build enterprise-grade APIs with proper architecture, validation, and error handling.