AuthN and AuthZ in NestJS using guards and decorators.

Guards in NestJS are responsible for determining whether a given request should be handled by the route handler or not. They are typically used for authentication and authorization purposes.

Karthick Ragavendran
6 min readNov 18, 2023

A Simple Auth Guard

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

@Injectable()
export class BasicAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// Simple authentication logic goes here
return true; // or false based on the logic
}
}

Imports

  • Injectable: Marks the class as a provider that can be managed by NestJS's dependency injection.
  • CanActivate: An interface that a guard class implements.
  • ExecutionContext: Provides details about the current request cycle.

The Guard Class:

  • BasicAuthGuard: The name of our custom guard class.

Dependency Injection:

  • The @Injectable() decorator allows NestJS to manage this class as a provider, enabling dependency injection if needed.

Implementing CanActivate

  • canActivate: This method is where the logic for guarding a route is implemented. It must return a boolean value.
  • The method receives an ExecutionContext instance, which provides details about the current request, response, and other contextual data.

Request Handling

  • context.switchToHttp().getRequest(): This extracts the request object from the current execution context. It's useful for accessing request details like headers, query parameters, or body.

Authentication Logic

  • Inside canActivate, you would implement your authentication logic. This could be as simple as checking a header or a token.
  • The return value (true or false) determines whether the request is allowed to proceed. true lets the request continue to the route handler, while false denies access.

Using the Guard

  • Apply the guard to routes using the @UseGuards decorator at the controller or route handler level.

Decorators

Before looking at our detailed AuthGuard, let's see how we will use that and hence understand what to implement inside the guard.

import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'
import { AuthGuard } from 'src/common/auth/auth.guard'

import { Role } from '@foundation/util/types'

export const AllowAuthenticated = (...roles: Role[]) =>
applyDecorators(SetMetadata('roles', roles), UseGuards(AuthGuard))

export const AllowAuthenticatedOptional = (...roles: Role[]) =>
applyDecorators(
SetMetadata('allowUnauthenticated', true),
SetMetadata('roles', roles),
UseGuards(AuthGuard),
)

The AllowAuthenticated decorator checks if the request is authenticated and also restricts access to certain roles. It throws an error back if the request is not authenticated.

  • applyDecorators: A NestJS function that combines multiple decorators into a single one.
  • SetMetadata: Used to define custom metadata. Here, it’s setting a ‘roles’ metadata with the roles passed as arguments.
  • UseGuards: It activates the specified guards, in this case, AuthGuard.
  • Usage: When applied, AllowAuthenticated ensures that only users with specified roles can access the route. The AuthGuard reads the 'roles' metadata and checks if the authenticated user has one of these roles.

AllowAuthenticatedOptional Decorator is similar to AllowAuthenticated, but with a key difference is the SetMetadata.

  • SetMetadata(‘allowUnauthenticated’, true) sets additional metadata that tells AuthGuard to allow unauthenticated access to the route. It's useful for routes which reacts to unauthenticated users differently than throwing an error.

Authguard requirements

  • Check for an INTERNAL_SECRET and bypass the check. This can be used by trusted internal parties where generating a JWT may not be possible.
  • AuthN: Get the BEARER eyblabla from the header and verify. Attach the information from token to the request as req.user. Get the metadata allowUnauthenticated to stop throwing unauthenticated error.
  • AuthZ: Get the roles metadata and check the database for the roles.
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { GqlExecutionContext } from '@nestjs/graphql'
import { JwtService } from '@nestjs/jwt'
import { PrismaService } from '../prisma/prisma.service'
import { Role } from '@foundation/util/types'

@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private jwtService: JwtService,
private prisma: PrismaService,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = GqlExecutionContext.create(context)
const req = ctx.getContext().req

if (this.bypassWithApiSecret(req)) {
// The auth check bypassed.
return true
}

await this.authenticateUser(req, context)

return this.authorizeUser(context, req.user) // Check roles
}

private bypassWithApiSecret(req: any) {
const apiSecret = req.headers['X-API-Secret']
if (!apiSecret) {
return false
}
if (apiSecret === process.env.INTERNAL_API_SECRET) {
return true
} else {
throw new ForbiddenException('Nope.')
}
}

private async authenticateUser(
req: any,
context: ExecutionContext,
): Promise<void> {
const bearerHeader = req.headers.authorization
const token = bearerHeader?.split(' ')[1]

if (!token) {
throw new UnauthorizedException('No token provided.')
}

try {
const user = await this.jwtService.verify(token, {
secret: process.env.NEXTAUTH_SECRET,
})
req.user = user
} catch (err) {
console.error('Token validation error:', err)
}

const allowUnauthenticated = this.getMetadata<boolean>(
'allowUnauthenticated',
context,
)

if (!req.user && !allowUnauthenticated) {
throw new UnauthorizedException()
}
}

private async authorizeUser(
context: ExecutionContext,
user: any,
): Promise<boolean> {
const requiredRoles = this.getMetadata<Role[]>('roles', context)

if (!requiredRoles || requiredRoles.length === 0) {
return true // No specific roles required, access is granted
}

const roleCheckPromises = requiredRoles.map((role) =>
this.userHasRequiredRole(user.uid, role),
)

const roleCheckResults = await Promise.all(roleCheckPromises)
return roleCheckResults.some(Boolean) // Return true if at least one role matches
}

// Helper methods

private async userHasRequiredRole(
uid: string,
requiredRole: Role,
): Promise<boolean> {
let userExists

switch (requiredRole) {
case 'admin':
userExists = await this.prisma.admin.findUnique({
where: { uid },
})
break
case 'manager':
userExists = await this.prisma.manager.findUnique({
where: { uid },
})
break
}

return Boolean(userExists)
}

private getMetadata<T>(key: string, context: ExecutionContext): T {
return this.reflector.getAllAndOverride<T>(key, [
context.getHandler(),
context.getClass(),
])
}
}

1. canActivate Method

  • Purpose: It’s the heart of the guard, determining whether the current request should proceed.
  • Process: It creates a GqlExecutionContext from the standard ExecutionContext to support both REST and GraphQL contexts. Then it checks for an API secret to potentially bypass further checks. If bypassing is not applicable, it authenticates the user (authenticateUser) and checks their authorization (authorizeUser).

2. bypassWithApiSecret Method

  • Purpose: Offers an alternative path for internal or trusted services, bypassing standard authentication.
  • Process: It checks the request headers for a specific API secret. If the secret matches the environment variable, access is granted. If the secret is present but incorrect, it throws a ForbiddenException.

3. authenticateUser Method

  • Purpose: To validate the user’s identity using a JWT token.
  • Process: It extracts the token from the request’s authorization header. If a token is present, it’s verified using JwtService. A valid token results in attaching the user information to the request (req.user). If authentication is optional (allowUnauthenticated), it allows proceeding without a valid user.

4. authorizeUser Method

  • Purpose: To ensure the authenticated user has the necessary roles for the requested route.
  • Process: It retrieves the required roles using getMetadata. If roles are specified, it checks whether the user has any of the required roles (userHasRequiredRole). Access is granted if the user possesses at least one of the necessary roles.

5. userHasRequiredRole Helper Method

  • Purpose: To verify if the authenticated user has a specific role.
  • Process: Based on the role requirement (like ‘admin’ or ‘teacher’), it queries the database (using Prisma) to confirm if the user exists in the respective role table. You can have your own logic here. The roles may be stored in a Roles table.

6. getMetadata Helper Method

  • Function: A generic method to fetch custom metadata set on route handlers or controller classes.
  • Use: It simplifies metadata retrieval, making the guard more adaptable and easier to maintain.

GetUser Parameter Decorator

We attached the user to the request (req.user). Let’s create a custom parameter decorator in NestJS that extracts the authenticated user’s details from a request, making them easily accessible within any controller or resolver method that utilizes this decorator.

export const GetUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const context = GqlExecutionContext.create(ctx)
const user = context.getContext().req.user
return user
},
)

Usage

  1. Authenticated users only.
@AllowAuthenticated()
@Query(() => [PremiumNews])
async premiumNews() {
return this.prisma.premiumNews.findMany()
}

2. Authenticated with roles. The authenticated users with roles admin or manager can access.

@AllowAuthenticated('admin', 'manager')
@Query(() => [User])
async users() {
return this.prisma.user.findMany()
}

3. Get data relevant to the requester. Let's say we have studentMe query. And we don't want to rely on the inputstudentId sent by the requester.

  @AllowAuthenticated()
@Query(() => Student, { name: 'studentMe' })
async studentMe(@GetUser() user: GetUserType) {
return this.prisma.student.findUnique({
where: { uid: user.uid },
})
}

4. AllowAuthenticatedOptional: Same as the above example but the query returns null instead of throwing an error.

@AllowAuthenticatedOptional()
@Query(() => Student, { name: 'studentMe' })
async studentMe(@GetUser() user: GetUserType) {
if(!user?.uid){
return null
}
return this.prisma.student.findUnique({
where: { uid: user.uid },
})
}

Remember, we need to decorate the query with @AllowAuthenticated before using the @Getuser parameter decorator.

Summary of Guard’s Workflow

  • Access Check: First, it checks for an API secret that can bypass other checks.
  • Authentication: Next, it verifies the user’s identity using a JWT.
  • Authorization: Then, it checks if the authenticated user has the required roles.
  • Decision: Finally, based on these checks, it decides whether to grant or deny access to the requested route.

This AuthGuard will be a comprehensive and helpful solution for managing both authentication and authorization in your NestJS application. It's designed to be flexible, catering to various authentication scenarios while ensuring that access control remains robust and secure.

This is the AuthN and AuthZ setup of my FoundationX full-stack monorepo boilerplate.

For more cool stuff have a look at my Github.

Happy coding 🎉

--

--

Karthick Ragavendran

Fullstack engineer | React, Typescript, Redux, JavaScript, UI, Storybook, CSS, UX, Cypress, CI/CD.