Building an Enterprise-Grade Authentication System: Custom Authentication Guard in NestJS

January 23, 2025

Introduction

In this blog post, we will create a reusable @Auth() decorator and guard in NestJS for streamlined role-based authorization. This will allow us to easily manage user roles and permissions in our application in a type-safe and maintainable way.

Authentication and authorization are crucial aspects of any web application. While NestJS provides built-in authentication mechanisms, implementing custom guards gives us more flexibility and control over how we handle user permissions. By the end of this tutorial, you'll have a robust authorization system that can be easily integrated into any NestJS application.

Prerequisites

Understanding the Architecture

Before we dive into the implementation, let's understand the key components we'll be building:

  1. @Auth() decorator - A custom decorator that combines role checking and session validation
  2. Guards - Implementation of the authorization logic
  3. Strategies - Core business logic for different types of authentication
  4. Supporting interfaces and constants

Setup

First, let's create out Authentication decorator. We'll use the applyDecorators function to apply the @UseGuards decorator to the class. This will allow us to use the SessionGuard and RolesStrategy guards in our controllers. then, let's create a auth/decorators/auth.decorator.ts file:

import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';
import { AuthDecoratorOptions } from '../auth.interface';
import { ROLES_KEY } from '../auth.constant';
import { SessionGuard } from '../guards/user-session.guard';
import { RolesGuard } from '../guards/roles.guard';
 
export function Auth(options?: AuthDecoratorOptions) {
  const { roles } = options;
  return applyDecorators(
    SetMetadata(ROLES_KEY, roles),
    UseGuards(SessionGuard, RolesGuard),
  );
}

This decorator combines multiple guards and metadata, making it easy to protect routes with a single annotation.

Then, let's create a auth/auth.constant.ts file to store our role constants:

export const ROLES_KEY = 'roles';

Now, let's create a auth/auth.interface.ts file to define our options:

export type Roles = Array<string>;
 
export interface AuthDecoratorOptions {
  roles?: Roles;
}

Let's create the strategies in auth/strategies. These will contain the core logic for our authorization system. First create a auth.strategy.ts file:

import { CanActivate, ExecutionContext } from '@nestjs/common';
export abstract class AuthStrategy implements CanActivate {
  public abstract canActivate(context: ExecutionContext): Promise<boolean>;
  protected req: Request | null = null;
  protected getRequest(context: ExecutionContext): Request {
    if (context.getType() === 'http') {
      this.req = context.switchToHttp().getRequest();
    }
    if (!this.req) {
      throw new Error('Request is not available');
    }
    return this.req;
  }
}

if you have graphql APIs, you can use the following code:

import { CanActivate, ExecutionContext } from '@nestjs/common';
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
export abstract class AuthStrategy implements CanActivate {
  public abstract canActivate(context: ExecutionContext): Promise<boolean>;
  protected req: Request | null = null;
  protected getRequest(context: ExecutionContext): Request {
    if (context.getType() === 'http') {
      this.req = context.switchToHttp().getRequest();
    } else if (context.getType<GqlContextType>() === 'graphql') {
      this.req = GqlExecutionContext.create(context).getContext().req;
     }
    // You can add other types here ex: websocket...
    if (!this.req) {
      throw new Error('Request is not available');
    }
    return this.req;
  }
}

The base strategy class provides common functionality for all our authentication strategies.

Next, create a roles.strategy.ts file for role-based authorization:

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../auth.interface';
import { ROLES_KEY } from '../auth.constant';
import { AuthStrategy } from './auth.strategy';
@Injectable()
export class RolesStrategy extends AuthStrategy {
  constructor(private readonly reflector: Reflector) {
    super();
  }
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = this.getRequest(context);
    const user = request['user']; // get user from the request object or from a different source e.g from session `req.session.user`
    const roles = this.reflector.get<Roles>(ROLES_KEY, context.getHandler());
    if (!roles?.length) {
      return true;
    }
    const userRoles = user?.roles;
    if (!userRoles) {
      // if user has no roles
      throw new UnauthorizedException('User has no roles assigned');
    }
    for (const role of roles) {
      // check if user has any of the roles
      if (!userRoles.includes(role)) {
        // if user doesn't have any of the roles
        throw new UnauthorizedException(`User lacks required role: ${role}`);
      }
    }
    return true;
  }
}

Create a session.strategy.ts file for session validation:

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { AuthStrategy } from './auth.strategy';
@Injectable()
export class SessionStrategy extends AuthStrategy {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = this.getRequest(context);
    const user = request['user'];
    if (!user) {
      throw new UnauthorizedException('No active session found');
    }
    return true;
  }
}

Now, we need to create guards for each of our strategies. Let's create a auth/guards/roles.guard.ts file:

import { Injectable } from '@nestjs/common';
import { RolesStrategy } from '../strategies/roles.strategy';
 
@Injectable()
export class RolesGuard extends RolesStrategy {}

Create a auth/guards/session.guard.ts file:

import { Injectable } from '@nestjs/common';
import { SessionStrategy } from '../strategies/session.strategy';
 
@Injectable()
export class SessionGuard extends SessionStrategy {}

Finally, let's create a auth/auth.module.ts file to configure our module:

import { Global, Module } from '@nestjs/common';
import { RolesStrategy } from './strategies/roles.strategy';
import { SessionStrategy } from './strategies/session.strategy';
 
const Strategies = [
  RolesStrategy,
  SessionStrategy,
  // add your strategies here...
];
@Global()
@Module({
  exports: [...Strategies],
  providers: [...Strategies],
})
export class AuthModule {}

Using the Auth Decorator

Before we can use our @Auth() decorator, we need to configure our authentication and authorization strategies. We can do this by adding the AuthModule to our application module:

@Module({
  imports: [AuthModule],
})
export class AppModule {}

Now that we have everything set up, we can use our @Auth() decorator in controllers like this:

@Controller('users')
export class UsersController {
  @Get()
  @Auth({ roles: ['ADMIN'] })
  async getUsers() {
    // Only accessible by admins
    return this.usersService.findAll();
  }
 
  @Post()
  @Auth({ roles: ['USER', 'ADMIN'] })
  async createUser() {
    // Accessible by users and admins
    return this.usersService.create();
  }
}

Error Handling

Our implementation includes proper error handling with descriptive messages:

Conclusion

In this tutorial, we've created a comprehensive authentication and authorization system using NestJS guards and decorators. This implementation provides:

The solution is production-ready and can be extended to support additional authentication mechanisms as needed.

Resources