Guards & Authorization
Guard system for authentication and authorization.
Overview
| Feature | Description |
|---|---|
| Built-in Guards | Ready-to-use auth, role, permission guards |
| RBAC | @RequireRoles() decorator |
| Permissions | @RequirePermissions() decorator |
| ABAC | RequirePolicy() for complex rules |
| Ownership | @RequireOwnership() for user resources |
| Composition | combineGuards(), sequenceGuards() |
| Conditional | whenGuard() for conditional execution |
| Caching | Request-scoped result caching |

Basic Usage
Authentication Guard
Require authentication for routes:
import { RequireAuthentication, Principal } from "@expressots/core";
import { controller, Get, principal } from "@expressots/adapter-express";
@controller("/api/users")
export class UserController {
@Get("/profile")
@RequireAuthentication()
getProfile(@principal() user: Principal) {
return { user: user.details };
}
}
Role-Based Access
Restrict access based on user roles:
import { RequireRoles } from "@expressots/core";
import { controller, Get } from "@expressots/adapter-express";
@controller("/api/admin")
export class AdminController {
@Get("/dashboard")
@RequireRoles("admin")
getDashboard() {
return { message: "Welcome, admin!" };
}
@Get("/super")
@RequireRoles("admin", "super-admin") // Any of these roles
getSuperDashboard() {
return { message: "Welcome, super admin!" };
}
}
Permission-Based Access
Control access with granular permissions:
import { RequirePermissions } from "@expressots/core";
import { controller, Get, Post, Delete, body, param } from "@expressots/adapter-express";
@controller("/api/documents")
export class DocumentController {
@Get("/")
@RequirePermissions("documents:read")
listDocuments() {
return this.documentService.findAll();
}
@Post("/")
@RequirePermissions("documents:write")
createDocument(@body() dto: CreateDocumentDto) {
return this.documentService.create(dto);
}
@Delete("/:id")
@RequirePermissions("documents:delete")
deleteDocument(@param("id") id: string) {
return this.documentService.delete(id);
}
}
Resource Ownership
Ensure users can only access their own resources:
import { RequireOwnership } from "@expressots/core";
import { controller, Get, Put, Delete, body, param } from "@expressots/adapter-express";
@controller("/api/posts")
export class PostController {
@Get("/:id")
@RequireOwnership("id") // Checks if user.id matches post.authorId
getPost(@param("id") id: string) {
return this.postService.findById(id);
}
@Put("/:id")
@RequireOwnership("id")
updatePost(@param("id") id: string, @body() dto: UpdatePostDto) {
return this.postService.update(id, dto);
}
@Delete("/:id")
@RequireOwnership("id")
deletePost(@param("id") id: string) {
return this.postService.delete(id);
}
}
Attribute-Based Access Control (ABAC)
Use RequirePolicy() for complex authorization rules:
import { UseGuards, RequireAuth, RequirePolicy } from "@expressots/core";
import { controller, Get, param } from "@expressots/adapter-express";
@controller("/api/resources")
export class ResourceController {
@Get("/:id")
@UseGuards(
RequireAuth(),
RequirePolicy((attrs) => {
// Complex authorization logic
const { user, resource } = attrs;
// Admin can access everything
if (user.roles.includes("admin")) {
return true;
}
// User can access their own resources
if (resource.owner === user.id) {
return true;
}
// Users can access public resources
if (resource.isPublic) {
return true;
}
return false;
})
)
getResource(@param("id") id: string) {
return this.resourceService.findById(id);
}
}
Guard Composition
Combining Guards
Require all guards to pass:
import { combineGuards, RequireAuth, RequireRole, RequirePermission } from "@expressots/core";
@Post("/critical")
@UseGuards(
combineGuards(
RequireAuth(),
RequireRole("admin"),
RequirePermission("critical:access")
)
)
criticalAction() {
return { message: "Critical action performed" };
}
Sequencing Guards
Execute guards in sequence with dependencies:
import { sequenceGuards } from "@expressots/core";
@Get("/data")
@UseGuards(
sequenceGuards(
AuthGuard, // First: authenticate
TenantGuard, // Second: verify tenant access
ResourceGuard // Third: verify resource access
)
)
getData() {}
Conditional Guards
Execute guards based on conditions:
import { whenGuard, RequireRole, RequireAuth } from "@expressots/core";
@Get("/data")
@Post("/data")
@UseGuards(
RequireAuth(),
// Only require admin role for POST requests
whenGuard(
ctx => ctx.request.method === "POST",
RequireRole("admin")
)
)
handleData() {}
Creating Custom Guards
Implement custom guards by implementing IGuard:
import { provide, inject, IGuard, GuardContext } from "@expressots/core";
@provide(CustomGuard)
export class CustomGuard implements IGuard {
constructor(@inject(PermissionService) private permissionService: PermissionService) {}
async canActivate(context: GuardContext): Promise<boolean> {
const { request, principal } = context;
// Custom authorization logic
const resourceId = request.params.id;
const user = principal.details as { id: string };
const hasAccess = await this.permissionService.checkAccess(
user.id,
resourceId
);
return hasAccess;
}
}
// Usage
@Get("/:id")
@UseGuards(CustomGuard)
getResource(@param("id") id: string) {}
Guard Decorator
Use @Guard() for auto-discovery:
import { Guard, IGuard, GuardContext } from "@expressots/core";
@Guard()
@provide(RateLimitGuard)
export class RateLimitGuard implements IGuard {
constructor(@inject(RateLimitService) private rateLimitService: RateLimitService) {}
async canActivate(context: GuardContext): Promise<boolean> {
const { request } = context;
const clientIp = request.ip;
return await this.rateLimitService.checkLimit(clientIp);
}
}
Guard Caching
Cache guard results for performance:
import { provide, inject, IGuard, IGuardCache, GuardContext, GuardResult } from "@expressots/core";
@provide(ExpensiveGuard)
export class ExpensiveGuard implements IGuard {
constructor(@inject("IGuardCache") private cache: IGuardCache) {}
async canActivate(context: GuardContext): Promise<GuardResult> {
const scope = context.getRequestId();
const cacheKey = `guard:${context.request.path}`;
// Check cache first
const cached = this.cache.get(scope, cacheKey);
if (cached !== null) {
return cached;
}
// Expensive check
const allowed = await this.expensivePermissionCheck(context);
const result = allowed ? GuardResult.allow() : GuardResult.deny();
// Cache for this request
this.cache.set(scope, cacheKey, result);
return result;
}
private async expensivePermissionCheck(context: GuardContext): Promise<boolean> {
// Complex logic...
return true;
}
}
Permission Hierarchy
Define permission inheritance:
import { setupAuthorizationForExpress } from "@expressots/adapter-express";
export class App extends AppExpress {
async configureServices(): Promise<void> {
setupAuthorizationForExpress(
this.container.Container,
{
permissionHierarchy: {
"super-admin": ["admin", "moderator", "user"],
"admin": ["moderator", "user"],
"moderator": ["user"],
},
},
this.Middleware,
MyAuthProvider
);
}
}
With this hierarchy:
super-adminhas all permissions ofadmin,moderator, anduseradminhas all permissions ofmoderatorandusermoderatorhas all permissions ofuser
Auth Provider
Implement an AuthProvider to integrate with your authentication system. The provider resolves a Principal for each request via getUser(), and the Principal itself answers the authorization questions:
import { provide } from "@expressots/core";
import { AuthProvider, Principal } from "@expressots/adapter-express";
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
interface JwtClaims {
sub: string;
roles: Array<string>;
permissions: Array<string>;
}
@provide(MyAuthProvider)
export class MyAuthProvider implements AuthProvider {
async getUser(
req: Request,
res: Response,
next: NextFunction
): Promise<Principal> {
const token = req.headers.authorization?.replace("Bearer ", "");
let claims: JwtClaims | null = null;
if (token) {
try {
claims = jwt.verify(token, process.env.JWT_SECRET!) as JwtClaims;
} catch {
claims = null;
}
}
return {
details: claims,
isAuthenticated: async () => claims !== null,
isInRole: async (role: string) =>
claims?.roles.includes(role) ?? false,
isResourceOwner: async (resourceId: unknown) => {
// Look up ownership in your data layer
return claims?.sub === String(resourceId);
},
};
}
}
Setup
Configure the authorization system in your application:
import { setupAuthorizationForExpress } from "@expressots/adapter-express";
import { MyAuthProvider } from "./auth/my-auth-provider";
export class App extends AppExpress {
async configureServices(): Promise<void> {
setupAuthorizationForExpress(
this.container.Container,
{
enablePreloading: true,
enableCaching: true,
permissionHierarchy: {
"super-admin": ["admin", "moderator", "user"],
"admin": ["moderator", "user"],
"moderator": ["user"],
},
},
this.Middleware,
MyAuthProvider
);
}
}
Testing Guards
Unit Testing Guards
Test guards in isolation:
import { CustomGuard } from "./custom.guard";
import { GuardContext } from "@expressots/core";
describe("CustomGuard", () => {
let guard: CustomGuard;
let mockPermissionService: jest.Mocked<PermissionService>;
beforeEach(() => {
mockPermissionService = {
checkAccess: jest.fn(),
} as any;
guard = new CustomGuard(mockPermissionService);
});
it("should allow access when user has permission", async () => {
mockPermissionService.checkAccess.mockResolvedValue(true);
const context = {
request: { params: { id: "123" } },
principal: { details: { id: "user-1" } },
} as unknown as GuardContext;
const result = await guard.canActivate(context);
expect(result).toBe(true);
expect(mockPermissionService.checkAccess).toHaveBeenCalledWith(
"user-1",
"123"
);
});
it("should deny access when user lacks permission", async () => {
mockPermissionService.checkAccess.mockResolvedValue(false);
const context = {
request: { params: { id: "123" } },
principal: { details: { id: "user-1" } },
} as unknown as GuardContext;
const result = await guard.canActivate(context);
expect(result).toBe(false);
});
});
Testing Built-in Guards
Test @RequireRoles() and @RequirePermissions():
import { RequireRole, RequirePermission } from "@expressots/core";
import { GuardContext } from "@expressots/core";
describe("Built-in Guards", () => {
describe("RequireRole", () => {
it("should allow access for users with required role", async () => {
const guard = RequireRole("admin");
const context = {
principal: {
isAuthenticated: async () => true,
isInRole: async (role: string) => role === "admin",
details: { roles: ["admin"] }
},
request: {},
} as unknown as GuardContext;
// Built-in guards return a GuardResult
const result = await guard.canActivate(context);
expect(result.allowed).toBe(true);
});
it("should deny access for users without required role", async () => {
const guard = RequireRole("admin");
const context = {
principal: {
isAuthenticated: async () => true,
isInRole: async () => false,
details: { roles: ["user"] }
},
request: {},
} as unknown as GuardContext;
const result = await guard.canActivate(context);
expect(result.allowed).toBe(false);
});
});
describe("RequirePermission", () => {
it("should allow access for users with required permission", async () => {
const guard = RequirePermission("documents:read");
// PermissionGuard falls back to principal.hasPermission()
// when no SecurityContext is bound
const context = {
principal: {
hasPermission: async (perm: string) => perm === "documents:read"
},
request: {},
} as unknown as GuardContext;
const result = await guard.canActivate(context);
expect(result.allowed).toBe(true);
});
});
});
Testing Guard Composition
Test combined guards:
import { combineGuards, RequireAuth, RequireRole, GuardContext, GuardResult } from "@expressots/core";
describe("Guard Composition", () => {
it("should require all guards to pass", async () => {
const combined = combineGuards(
RequireAuth(),
RequireRole("admin")
);
// User is authenticated but not admin
const context = {
principal: {
isAuthenticated: async () => true,
isInRole: async () => false,
details: { roles: ["user"] }
},
request: {},
} as unknown as GuardContext;
const result = (await combined.canActivate(context)) as GuardResult;
expect(result.allowed).toBe(false);
});
it("should pass when all guards pass", async () => {
const combined = combineGuards(
RequireAuth(),
RequireRole("admin")
);
const context = {
principal: {
isAuthenticated: async () => true,
isInRole: async (role: string) => role === "admin",
details: { roles: ["admin"] }
},
request: {},
} as unknown as GuardContext;
const result = (await combined.canActivate(context)) as GuardResult;
expect(result.allowed).toBe(true);
});
});
Testing Auth Provider
Test your custom AuthProvider:
import { MyAuthProvider } from "./my-auth-provider";
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
jest.mock("jsonwebtoken");
describe("MyAuthProvider", () => {
let provider: MyAuthProvider;
const res = {} as Response;
const next = jest.fn() as NextFunction;
beforeEach(() => {
jest.resetAllMocks();
provider = new MyAuthProvider();
});
it("should resolve an authenticated principal for a valid token", async () => {
(jwt.verify as jest.Mock).mockReturnValue({
sub: "user-123",
roles: ["user"],
permissions: ["read"],
});
const request = {
headers: { authorization: "Bearer valid-token" },
} as Request;
const principal = await provider.getUser(request, res, next);
expect(await principal.isAuthenticated()).toBe(true);
expect(await principal.isInRole("user")).toBe(true);
expect(principal.details).toMatchObject({ sub: "user-123" });
});
it("should resolve an unauthenticated principal for an invalid token", async () => {
(jwt.verify as jest.Mock).mockImplementation(() => {
throw new Error("Invalid token");
});
const request = {
headers: { authorization: "Bearer invalid-token" },
} as Request;
const principal = await provider.getUser(request, res, next);
expect(await principal.isAuthenticated()).toBe(false);
});
it("should resolve an unauthenticated principal when no token provided", async () => {
const request = {
headers: {},
} as Request;
const principal = await provider.getUser(request, res, next);
expect(await principal.isAuthenticated()).toBe(false);
});
});
E2E Testing with Guards
Test guards in a real application:
import { bootstrap } from "@expressots/core";
import { App } from "./app";
describe("Guards E2E", () => {
let app: Awaited<ReturnType<typeof bootstrap>>;
let port: number;
beforeAll(async () => {
app = await bootstrap(App, { port: 0 });
port = await app.getPort();
});
afterAll(async () => {
const httpServer = await app.getHttpServer();
await new Promise<void>((resolve) => {
httpServer.close(() => resolve());
});
});
it("should block unauthenticated requests", async () => {
const response = await fetch(`http://localhost:${port}/api/users/profile`);
expect(response.status).toBe(401);
});
it("should allow authenticated requests", async () => {
const token = await getTestToken({ roles: ["user"] });
const response = await fetch(
`http://localhost:${port}/api/users/profile`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
expect(response.status).toBe(200);
});
it("should block requests without required role", async () => {
const token = await getTestToken({ roles: ["user"] });
const response = await fetch(
`http://localhost:${port}/api/admin/dashboard`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
expect(response.status).toBe(403);
});
it("should allow requests with required role", async () => {
const token = await getTestToken({ roles: ["admin"] });
const response = await fetch(
`http://localhost:${port}/api/admin/dashboard`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
expect(response.status).toBe(200);
});
it("should enforce resource ownership", async () => {
const token = await getTestToken({ userId: "user-1" });
// Try to access another user's post
const response = await fetch(
`http://localhost:${port}/api/posts/user-2-post`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
expect(response.status).toBe(403);
});
});
async function getTestToken(payload: any): Promise<string> {
// Generate a test JWT token
return jwt.sign(payload, process.env.JWT_SECRET);
}
Real-World RBAC Examples
Multi-Tenant Application
@controller("/api/tenants/:tenantId")
export class TenantController {
@Get("/data")
@UseGuards(
RequireAuth(),
combineGuards(
TenantAccessGuard, // Verify user belongs to tenant
RequireRole("admin", "member")
)
)
getTenantData(@param("tenantId") tenantId: string, @principal() user: Principal) {
return this.tenantService.getData(tenantId);
}
@Post("/invite")
@UseGuards(
RequireAuth(),
TenantAccessGuard,
RequireRole("admin", "owner") // Only admins/owners can invite
)
inviteUser(@param("tenantId") tenantId: string, @body() dto: InviteDto) {
return this.tenantService.inviteUser(tenantId, dto);
}
}
Content Management System
@controller("/api/content")
export class ContentController {
@Get("/")
@UseGuards(RequirePermission("content:read"))
listContent() {
return this.contentService.findAll();
}
@Post("/")
@UseGuards(
combineGuards(
RequireAuth(),
RequirePermission("content:create")
)
)
createContent(@body() dto: CreateContentDto) {
return this.contentService.create(dto);
}
@Put("/:id")
@UseGuards(
RequireAuth(),
// Admins need the update permission
whenGuard(
(ctx) => ctx.principal.isInRole("admin"),
RequirePermission("content:update")
),
// Non-admins must own the content
whenGuard(
async (ctx) => !(await ctx.principal.isInRole("admin")),
RequireResourceOwner("id")
)
)
updateContent(@param("id") id: string, @body() dto: UpdateContentDto) {
return this.contentService.update(id, dto);
}
@Delete("/:id")
@UseGuards(
combineGuards(
RequireAuth(),
RequirePermission("content:delete"),
// Non-admins must also own the content
whenGuard(
async (ctx) => !(await ctx.principal.isInRole("admin")),
RequireResourceOwner("id")
)
)
)
deleteContent(@param("id") id: string) {
return this.contentService.delete(id);
}
}
Healthcare Application (HIPAA Compliant)
@controller("/api/patients")
export class PatientController {
@Get("/:id")
@UseGuards(
RequireAuth(),
combineGuards(
RequireRole("doctor", "nurse", "admin"),
RequirePermission("patients:read"),
PatientAccessGuard // Verify provider can access this patient
)
)
getPatient(@param("id") id: string) {
return this.patientService.findById(id);
}
@Post("/:id/prescriptions")
@UseGuards(
RequireAuth(),
combineGuards(
RequireRole("doctor"), // Only doctors can prescribe
RequirePermission("prescriptions:write"),
PatientAccessGuard,
LicenseValidGuard // Verify doctor's license is valid
)
)
createPrescription(@param("id") patientId: string, @body() dto: PrescriptionDto) {
return this.prescriptionService.create(patientId, dto);
}
}
Security Best Practices
- Always Authenticate First: Use
RequireAuth()before other guards - Principle of Least Privilege: Grant minimum permissions needed
- Use Permission Hierarchy: Define role inheritance to reduce configuration
- Log Authorization Failures: Monitor failed authorization attempts
- Cache Carefully: Be cautious when caching authorization results
- Test Edge Cases: Test boundary conditions and permission combinations
- Use HTTPS: Always use HTTPS in production to protect tokens
- Rotate Secrets: Regularly rotate JWT secrets and API keys
- Implement Rate Limiting: Prevent brute force attacks on protected endpoints
- Audit Access: Log all access to sensitive resources
Common Security Patterns
Two-Factor Authentication
@Post("/sensitive-action")
@UseGuards(
RequireAuth(),
RequireTwoFactor(), // Custom guard to verify 2FA
RequireRole("admin")
)
performSensitiveAction() {
return { success: true };
}
IP Whitelisting
@Post("/admin/config")
@UseGuards(
RequireAuth(),
RequireRole("admin"),
IPWhitelistGuard // Only allow from specific IPs
)
updateConfig(@body() config: ConfigDto) {
return this.configService.update(config);
}
Time-Based Access
@Get("/reports")
@UseGuards(
RequireAuth(),
RequireRole("analyst"),
BusinessHoursGuard // Only allow during business hours
)
getReports() {
return this.reportService.generate();
}
Best Practices
- Use Built-in Guards: Start with built-in guards before creating custom ones
- Combine Guards: Use
combineGuards()for multiple requirements - Use Conditions: Use
whenGuard()for conditional authorization - Cache Results: Use
IGuardCachefor expensive checks - Define Hierarchy: Set up permission hierarchy to reduce configuration
- Implement AuthProvider: Integrate with your auth system properly
- Test Thoroughly: Test all authorization paths and edge cases
- Monitor Access: Log and monitor authorization failures
- Use Granular Permissions: Prefer fine-grained permissions over broad roles
- Document Permissions: Maintain clear documentation of all permissions
Comparison with Other Frameworks
| Feature | ExpressoTS | NestJS | Spring Boot |
|---|---|---|---|
| Built-in Guards | ✅ 5+ guards | ⚠️ Basic guards | ✅ Via Security |
| Guard Composition | ✅ combineGuards() | ❌ Manual | ❌ Manual |
| Conditional Guards | ✅ whenGuard() | ❌ Not available | ❌ Not available |
| Guard Caching | ✅ IGuardCache | ❌ Not available | ❌ Not available |
| Permission Hierarchy | ✅ Built-in | ❌ Manual | ✅ Via Security |
| ABAC Support | ✅ RequirePolicy() | ❌ Manual | ⚠️ SpEL expressions |
Support the Project
ExpressoTS is MIT-licensed open source. See the support guide to contribute.