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.
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
orfalse
) determines whether the request is allowed to proceed.true
lets the request continue to the route handler, whilefalse
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. TheAuthGuard
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 asreq.user
. Get the metadataallowUnauthenticated
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 standardExecutionContext
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
- 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 🎉