Authentication and Authorization in Full-Stack Applications with NestJS
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.
What is Authentication vs Authorization?
Before diving in, let’s clarify the difference:
- Authentication: Verifying a user's identity (e.g., logging in).
- Authorization: Determining what resources an authenticated user can access.
Setting Up a NestJS Project
If you don’t already have a NestJS project, create one:
npm i -g @nestjs/cli
nest new my-nest-app
cd my-nest-appInstall the necessary packages for authentication:
npm install @nestjs/passport passport passport-jwt jsonwebtoken bcryptjs
npm install --save-dev @types/passport-jwt @types/bcryptjsImplementing Authentication
We'll use JWT (JSON Web Tokens) for stateless authentication. Here's a step-by-step guide:
1. Create an AuthModule
nest generate module auth
nest generate service auth
nest generate controller auth2. Create a User Entity
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;
}3. Create a User Service
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);
}
}4. Implement JWT Strategy
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 };
}
}5. Create Authentication Endpoints
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);
}
}6. Secure Routes
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.' };
}
}Adding Authorization
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.' };
}Conclusion
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.