Adding Fine-Grained Authorization to a NestJS API
A step-by-step guide to wiring WardenAuth into a NestJS application: registering the module, building a permission guard, using a @RequirePermission decorator, and checking access inside services — with dependency injection throughout.
This guide adds fine-grained, externalized authorization to a NestJS API using WardenAuth’s NestJS module. By the end you will have a reusable @RequirePermission() decorator, a guard that enforces it, and service-level checks — all wired through Nest’s dependency injection.
request.user.Step 1: Install
npm install @ecarrizo2/wardenauthz-nestjsStep 2: Register the Module
// app.module.ts
import { Module } from '@nestjs/common'
import { WardenAuthModule } from '@ecarrizo2/wardenauthz-nestjs'
@Module({
imports: [
WardenAuthModule.forRoot({
apiKey: process.env.RBAC_API_KEY!,
baseUrl: process.env.RBAC_API_URL!,
}),
],
})
export class AppModule {}Step 3: A Permission Decorator
Define metadata that says which permission an endpoint requires:
// require-permission.decorator.ts
import { SetMetadata } from '@nestjs/common'
export interface PermissionRule { resource: string; action: string }
export const PERMISSION_KEY = 'required_permission'
export const RequirePermission = (resource: string, action: string) =>
SetMetadata(PERMISSION_KEY, { resource, action } as PermissionRule)Step 4: The Guard
The guard reads the metadata, resolves the scope and subject, and asks WardenAuth for a decision:
// permission.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { WardenAuthService } from '@ecarrizo2/wardenauthz-nestjs'
import { PERMISSION_KEY, PermissionRule } from './require-permission.decorator'
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly accessControl: WardenAuthService,
) {}
async canActivate(ctx: ExecutionContext): Promise<boolean> {
const rule = this.reflector.get<PermissionRule>(PERMISSION_KEY, ctx.getHandler())
if (!rule) return true // no rule = public
const req = ctx.switchToHttp().getRequest()
const scopeId = req.headers['x-workspace-id']
const { allowed } = await this.accessControl.accessCheck.check({
subjectId: req.user.sub,
scopeId,
resource: rule.resource,
action: rule.action,
})
if (!allowed) throw new ForbiddenException()
return true
}
}Step 5: Protect Endpoints
// invoices.controller.ts
@Controller('invoices')
@UseGuards(PermissionGuard)
export class InvoicesController {
@Delete(':id')
@RequirePermission('invoice', 'delete')
async remove(@Param('id') id: string) {
return this.invoicesService.remove(id)
}
}Step 6: Checks Inside Services
For conditional logic that is not a simple gate, inject the service directly:
@Injectable()
export class ReportsService {
constructor(private readonly accessControl: WardenAuthService) {}
async export(userId: string, scopeId: string) {
const { allowed } = await this.accessControl.accessCheck.check({
subjectId: userId, scopeId, resource: 'report', action: 'export',
})
if (!allowed) throw new ForbiddenException()
// ...generate export
}
}Summary
You now have declarative, externalized authorization in NestJS — one decorator per endpoint, zero business logic in your handlers, and a single source of truth in WardenAuth. Grab an API key and wire up your first scope.