Rok Mohar
Rok Mohar

Jan 27, 2025

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-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

Implementing 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 auth

2. 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.