Jan 27, 2025
Authentication and authorization are essential for modern web applications. They ensure that only authorized users can access your app’s resources, enhancing security and user experience. In this blog post, we'll explore how to handle authentication and authorization in a full-stack application using NestJS, a popular Node.js framework for building scalable and maintainable server-side applications.
Before diving in, let’s clarify the difference:
If you don’t already have a NestJS project, create one:
npm i -g @nestjs/cli
nest new my-nest-app
cd my-nest-app
Install the necessary packages for authentication:
npm install @nestjs/passport passport passport-jwt jsonwebtoken bcryptjs
npm install --save-dev @types/passport-jwt @types/bcryptjs
We'll use JWT (JSON Web Tokens) for stateless authentication. Here's a step-by-step guide:
nest generate module auth
nest generate service auth
nest generate controller auth
Define a user entity in src/user/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ default: 'user' })
role: string;
}
Implement methods for user management, such as finding a user by email or creating new users.
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcryptjs';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async findByEmail(email: string): Promise<User | undefined> {
return this.userRepository.findOne({ where: { email } });
}
async createUser(email: string, password: string): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 10);
const user = this.userRepository.create({ email, password: hashedPassword });
return this.userRepository.save(user);
}
}
Create a strategy for validating JWT tokens:
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}
Define login and registration endpoints in AuthController:
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserService } from '../user/user.service';
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private userService: UserService,
) {}
@Post('register')
async register(@Body() body: { email: string; password: string }) {
return this.userService.createUser(body.email, body.password);
}
@Post('login')
async login(@Body() body: { email: string; password: string }) {
return this.authService.login(body.email, body.password);
}
}
Use @UseGuards(AuthGuard('jwt'))
to protect endpoints:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('protected')
export class ProtectedController {
@UseGuards(AuthGuard('jwt'))
@Get()
getProtectedData() {
return { message: 'This is a protected route.' };
}
}
To manage roles, you can create a custom guard:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
Apply the guard to routes:
import { SetMetadata, UseGuards } from '@nestjs/common';
import { RolesGuard } from './roles.guard';
@UseGuards(AuthGuard('jwt'), RolesGuard)
@SetMetadata('roles', ['admin'])
@Get('admin')
getAdminData() {
return { message: 'This is an admin-only route.' };
}
By following these steps, you can implement robust authentication and authorization in your full-stack application using NestJS. With JWT handling authentication and custom guards managing authorization, you’ll be well-equipped to secure your app’s resources while maintaining scalability.