How to protect endpoints of a Nestjs application

Security is a major concern when writing a web service. Nestjs provides awesome features that enables developers to structure code in a very declarative way. For instance, one can protect appliations against unauthorized access by using Guards. Guards are injectable blocks that can be added to protect a specific route, an entire controller or an entire application.

By default, I like to put guards at the top-most level possible. Let's imagine for a moment that your service is protected by an authentication layer, let's say cookie based. Putting the whole API behind an authentication guard is the easiest way to secure it against unauthorized access. You can do that in Nestjs with:

import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './guards/auth-guard';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
 
  // here we guard the whole application with one Guard
  app.useGlobalGuards(new AuthGuard());
 
  await app.listen(3000);
}

bootstrap();

Any access to the routes provided by the application is going through this guard, enabling you to protect them. Here is the example of a very dummy guard, where nobody can access anything, as we return false every time:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';


@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return false;
  }
}

However, there are several use cases where you need to disable the guard from checking for authorization. For instance, if you have a /login endpoint. In that case, you need to specifically tell the guard to not do anything for this route. You preferably should do it the Nestjs way. Here is one.

Guards can access the execution context. The execution context contains information about the request and can be used to create guards, filters and interceptors. Nestjs provides a decorator SetMetadata that you can add to endpoints to add metadata to the execution context. Therefore, if we add information on a specific route or controller, we can retrieve it from the guard.

Let's consider the following controller:

import { Controller, Get, SetMetadata } from '@nestjs/common';
import { AuthGuardConfig, AUTH_GUARD_CONFIG } from './guards/auth-guard';

@Controller()
export class AppController {
  @Get('guarded')
  guarded(): string {
    return 'This is open.';
  }

  @Get('open')
  @SetMetadata(AUTH_GUARD_CONFIG, { disabled: true } as AuthGuardConfig)
  open(): string {
    return 'This is open.';
  }
}

The first endpoint /guarded is guarded by the guard by default. The second one is not, as specified by the @SetMetadata(AUTH_GUARD_CONFIG, { disabled: true }).

Here is the code of the guard:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

export interface AuthGuardConfig {
  disabled?: boolean;
}

export const AUTH_GUARD_CONFIG = Symbol('AUTH_GUARD_CONFIG');

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const handlerConfig = this.reflector.get<AuthGuardConfig>(
      AUTH_GUARD_CONFIG,
      context.getHandler(),
    );
    const controllerConfig = this.reflector.get<AuthGuardConfig>(
      AUTH_GUARD_CONFIG,
      context.getClass(),
    );
    if (controllerConfig?.disabled || handlerConfig?.disabled) {
      return true;
    }
    return false;
  }
}

The guard uses the reflector to get ahold of the metadata symbol AUTH_GUARD_CONFIG. It does it at the controller level with context.getClass()and at the method handler level context.getHandler() If a route specifies this metadata and puts the disabled parameter to true, the guard will read it and return true and therefore authorize the access. If a controller does it, all the routes of this controller will be authorized.

The last remaining piece to adjust is that your guard, when declared globally for your app, needs to access the reflector. This is how you can achieve it:

import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './guards/auth-guard';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalGuards(new AuthGuard(app.get(Reflector)));
  await app.listen(3000);
}

bootstrap();

I have put a link to a Github repository for easier reference and hope it helps.

GitHub - kevinmerckx/app-guard-nestjs
Contribute to kevinmerckx/app-guard-nestjs development by creating an account on GitHub.
Update from 23.12.2023: I wrote a possible enhancement to the SetMetadata in this article https://blog.merckx.fr/how-to-protect-endpoints-of-a-nestjs-application-revisited/

KM

Photo by Rowan Heuvel on Unsplash

Kevin Merckx

Kevin Merckx

Software Engineer, Real Full Stack Developer: from software architecture to development operations and programming.