Guards
Guards are the mechanism responsible for Authorization and Access Control. Their sole responsibility is to determine whether a given request should be allowed to proceed to the route handler.
Guards are executed after all middleware but before any interceptor or checking of pipes.
Concept: The Bouncer
A Guard acts as the Security / Bouncer at the restaurant door.
- They check ID (Authentication).
- They check the guest list (Authorization/Roles).
- If the Bouncer says "No", the customer never reaches the Waiter. The request is rejected immediately.
The CanActivate Interface
A Guard is a class annotated with @injectable() that implements the CanActivate interface. It must define a canActivate() method.
This method receives the ExecutionContext and must return:
true: The request is allowed.false: The request is denied (Karin automatically throws403 Forbidden).Promise<boolean>: For asynchronous validation (e.g., database checks).
Example: Basic Authentication Guard
This guard checks for the presence of an Authorization header.
import {
CanActivate,
ExecutionContext,
injectable,
UnauthorizedException,
} from "@project-karin/core";
@injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.get("Authorization");
if (!token) {
// It is best practice to throw a specific exception rather than return false
// to provide a clearer error message (401 vs 403).
throw new UnauthorizedException("Missing Authorization Token");
}
// In a real app, validate the token here
return true;
}
}Execution Context
The ExecutionContext is a powerful utility that provides access to the request details irrespective of the underlying protocol (HTTP, WebSocket, RPC).
const request = context.switchToHttp().getRequest();
const handler = context.getHandler(); // The method about to be called
const controller = context.getClass(); // The class of the methodThis context allows guards to make decisions not just based on the request, but based on where the request is going.
Role-Based Access Control (RBAC)
Guards are most powerful when combined with custom metadata. A common pattern is restricting routes based on user roles.
1. Setting Context (Metadata)
First, the route is tagged with the required roles (see decorators.md).
@Roles('admin')
@Get('delete')
deleteEverything() { ... }2. Reading Context (Reflector)
The Guard uses the Reflector helper to read this metadata dynamically.
import { Reflector } from "@project-karin/core";
@injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 1. Retrieve the roles required by the handler
const requiredRoles = this.reflector.get<string[]>(
"roles",
context.getHandler(),
);
// 2. If no roles are required, allow access
if (!requiredRoles) {
return true;
}
// 3. Retrieve the user (attached by a previous AuthGuard)
const request = context.switchToHttp().getRequest();
const user = request.user;
// 4. Validate
return requiredRoles.some((role) => user?.roles?.includes(role));
}
}Binding Guards
Guards can be bound at three different scopes:
1. Method Scope
Applies only to a specific route.
@Get()
@UseGuards(RolesGuard)
findAll() { ... }2. Controller Scope
Applies to every route in the controller.
@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController { ... }3. Global Scope
Applies to every route in the entire application.
// main.ts
const app = await KarinFactory.create(adapter, {
globalGuards: [new AuthGuard()], // Apply to everything
});WARNING
Global guards registered via the globalGuards array are instantiated outside the DI container context in some edge configurations. If your global guard has complex dependencies, ensure it is registered via a provider pattern or that the dependencies are available.
