Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
- Basic Node.js and Express knowledge ๐ฅ๏ธ
What you'll learn
- Understand role-based authorization fundamentals ๐ฏ
- Apply RBAC in real projects ๐๏ธ
- Debug common authorization issues ๐
- Write type-safe authorization code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on role-based authorization! ๐ In this guide, weโll explore how to build secure, scalable authorization systems that control who can access what in your applications.
Youโll discover how role-based access control (RBAC) can transform your TypeScript applications from security nightmares into fortress-like systems. Whether youโre building APIs ๐, admin dashboards ๐ฅ๏ธ, or user management systems ๐ฅ, understanding RBAC is essential for protecting your applicationโs resources.
By the end of this tutorial, youโll feel confident implementing rock-solid authorization in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Role-Based Authorization
๐ค What is Role-Based Authorization?
Role-based authorization is like a VIP club system ๐ญ. Think of it as a bouncer at a nightclub who checks your membership card before letting you into different areas. Some people get basic access to the main floor, while others get VIP access to exclusive areas.
In TypeScript terms, RBAC controls what authenticated users can access based on their assigned roles ๐ก๏ธ. This means you can:
- โจ Control access to specific resources
- ๐ Scale permissions across large teams
- ๐ก๏ธ Maintain security without complexity
- ๐ Audit who accessed what and when
๐ก Why Use Role-Based Authorization?
Hereโs why developers love RBAC:
- Scalability ๐: Manage permissions for thousands of users
- Maintainability ๐ง: Change role permissions once, affect all users
- Security ๐: Principle of least privilege built-in
- Clarity ๐๏ธ: Clear understanding of who can do what
Real-world example: Imagine building a hospital management system ๐ฅ. With RBAC, doctors can access patient records, nurses can update vital signs, and admin staff can only view billing information.
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly authorization system:
// ๐ Hello, secure TypeScript!
enum Role {
ADMIN = "admin", // ๐ Full access
USER = "user", // ๐ค Basic access
GUEST = "guest" // ๐ View only
}
// ๐จ Creating a user type
interface User {
id: string;
name: string;
email: string;
role: Role; // ๐ญ Their assigned role
isActive: boolean; // โ
Account status
}
// ๐ก๏ธ Authorization decorator
function requireRole(allowedRoles: Role[]) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const user: User = this.currentUser; // ๐ค Get current user
if (!allowedRoles.includes(user.role)) {
throw new Error(`๐ซ Access denied. Required roles: ${allowedRoles.join(', ')}`);
}
return method.apply(this, args);
};
};
}
๐ก Explanation: The requireRole
decorator checks if the current user has permission before executing any method!
๐ฏ Common Authorization Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Resource-based permissions
interface Permission {
resource: string; // ๐ What resource (users, posts, etc.)
action: string; // โก What action (read, write, delete)
conditions?: any; // ๐ฏ Optional conditions
}
// ๐จ Pattern 2: Role definitions
interface RoleDefinition {
name: Role;
permissions: Permission[];
description: string;
}
// ๐ Pattern 3: Authorization service
class AuthorizationService {
private rolePermissions = new Map<Role, Permission[]>();
// ๐ฏ Check if user can perform action
canAccess(user: User, resource: string, action: string): boolean {
const permissions = this.rolePermissions.get(user.role) || [];
return permissions.some(p =>
p.resource === resource && p.action === action
);
}
}
๐ก Practical Examples
๐ฅ Example 1: Hospital Management System
Letโs build something real and important:
// ๐ฅ Define hospital roles
enum HospitalRole {
DOCTOR = "doctor",
NURSE = "nurse",
ADMIN = "admin",
PATIENT = "patient"
}
// ๐งโโ๏ธ Hospital user
interface HospitalUser {
id: string;
name: string;
role: HospitalRole;
department?: string; // ๐ข Which department
licenseNumber?: string; // ๐ Professional license
}
// ๐ Patient record interface
interface PatientRecord {
id: string;
patientName: string;
diagnosis: string;
medications: string[];
assignedDoctor: string;
isConfidential: boolean; // ๐ Sensitive cases
}
// ๐ก๏ธ Hospital authorization service
class HospitalAuth {
private currentUser: HospitalUser;
constructor(user: HospitalUser) {
this.currentUser = user;
}
// ๐ Can read patient records?
canReadPatientRecord(record: PatientRecord): boolean {
switch (this.currentUser.role) {
case HospitalRole.DOCTOR:
return record.assignedDoctor === this.currentUser.id || !record.isConfidential;
case HospitalRole.NURSE:
return !record.isConfidential; // ๐ซ No confidential records
case HospitalRole.ADMIN:
return true; // ๐ Full access
case HospitalRole.PATIENT:
return false; // ๐ซ Patients can't read others' records
default:
return false;
}
}
// โ๏ธ Can modify patient records?
canModifyPatientRecord(record: PatientRecord): boolean {
switch (this.currentUser.role) {
case HospitalRole.DOCTOR:
return record.assignedDoctor === this.currentUser.id;
case HospitalRole.NURSE:
return true; // โ
Nurses can update vital signs
case HospitalRole.ADMIN:
return true; // ๐ Full access
default:
return false; // ๐ซ Others can't modify
}
}
// ๐ Can prescribe medications?
canPrescribeMedication(): boolean {
return this.currentUser.role === HospitalRole.DOCTOR &&
!!this.currentUser.licenseNumber; // ๐ Must have license
}
}
// ๐ฎ Let's use it!
const drSmith: HospitalUser = {
id: "doc001",
name: "Dr. Smith",
role: HospitalRole.DOCTOR,
department: "Cardiology",
licenseNumber: "MD123456"
};
const nurseJones: HospitalUser = {
id: "nurse001",
name: "Nurse Jones",
role: HospitalRole.NURSE,
department: "Emergency"
};
const patientRecord: PatientRecord = {
id: "patient001",
patientName: "John Doe",
diagnosis: "Hypertension",
medications: ["Lisinopril"],
assignedDoctor: "doc001",
isConfidential: false
};
// ๐งช Test authorization
const doctorAuth = new HospitalAuth(drSmith);
const nurseAuth = new HospitalAuth(nurseJones);
console.log("๐จโโ๏ธ Doctor can read:", doctorAuth.canReadPatientRecord(patientRecord)); // โ
true
console.log("๐ฉโโ๏ธ Nurse can read:", nurseAuth.canReadPatientRecord(patientRecord)); // โ
true
console.log("๐ Doctor can prescribe:", doctorAuth.canPrescribeMedication()); // โ
true
console.log("๐ Nurse can prescribe:", nurseAuth.canPrescribeMedication()); // โ false
๐ฏ Try it yourself: Add a canViewBilling
method that only allows admin and billing staff!
๐ช Example 2: E-commerce Admin System
Letโs make it practical for business:
// ๐ E-commerce roles
enum EcommerceRole {
SUPER_ADMIN = "super_admin",
STORE_ADMIN = "store_admin",
INVENTORY_MANAGER = "inventory_manager",
CUSTOMER_SERVICE = "customer_service",
VIEWER = "viewer"
}
// ๐ฆ Product interface
interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
isActive: boolean;
}
// ๐ญ Permission system
class EcommercePermissions {
private static permissions = new Map<EcommerceRole, string[]>([
[EcommerceRole.SUPER_ADMIN, ["*"]], // ๐ Everything
[EcommerceRole.STORE_ADMIN, [
"products:read", "products:write", "products:delete",
"orders:read", "orders:write",
"users:read"
]],
[EcommerceRole.INVENTORY_MANAGER, [
"products:read", "products:write",
"inventory:read", "inventory:write"
]],
[EcommerceRole.CUSTOMER_SERVICE, [
"orders:read", "orders:write",
"users:read", "returns:write"
]],
[EcommerceRole.VIEWER, ["products:read", "orders:read"]]
]);
// ๐ Check permission
static hasPermission(role: EcommerceRole, permission: string): boolean {
const rolePermissions = this.permissions.get(role) || [];
// ๐ Super admin has everything
if (rolePermissions.includes("*")) {
return true;
}
return rolePermissions.includes(permission);
}
// ๐ฏ Require permission decorator
static requirePermission(permission: string) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const user = this.currentUser;
if (!EcommercePermissions.hasPermission(user.role, permission)) {
throw new Error(`๐ซ Permission denied: ${permission}`);
}
return method.apply(this, args);
};
};
}
}
// ๐ช Product service with authorization
class ProductService {
currentUser: { role: EcommerceRole; name: string };
constructor(user: { role: EcommerceRole; name: string }) {
this.currentUser = user;
}
// ๐ Read products (most roles can do this)
@EcommercePermissions.requirePermission("products:read")
getProducts(): Product[] {
console.log(`๐ ${this.currentUser.name} is viewing products`);
return [
{ id: "1", name: "TypeScript Book", price: 29.99, stock: 100, category: "books", isActive: true }
];
}
// โ๏ธ Create product (restricted)
@EcommercePermissions.requirePermission("products:write")
createProduct(product: Omit<Product, "id">): Product {
const newProduct: Product = {
...product,
id: Math.random().toString(36).substr(2, 9)
};
console.log(`โจ ${this.currentUser.name} created product: ${newProduct.name}`);
return newProduct;
}
// ๐๏ธ Delete product (very restricted)
@EcommercePermissions.requirePermission("products:delete")
deleteProduct(productId: string): void {
console.log(`๐๏ธ ${this.currentUser.name} deleted product: ${productId}`);
}
}
// ๐ฎ Test different user roles
const superAdmin = { role: EcommerceRole.SUPER_ADMIN, name: "Alice" };
const inventoryManager = { role: EcommerceRole.INVENTORY_MANAGER, name: "Bob" };
const viewer = { role: EcommerceRole.VIEWER, name: "Charlie" };
const adminService = new ProductService(superAdmin);
const inventoryService = new ProductService(inventoryManager);
const viewerService = new ProductService(viewer);
try {
// โ
These will work
adminService.getProducts();
inventoryService.getProducts();
viewerService.getProducts();
// โ
These will work
adminService.createProduct({ name: "New Book", price: 19.99, stock: 50, category: "books", isActive: true });
inventoryService.createProduct({ name: "Another Book", price: 24.99, stock: 30, category: "books", isActive: true });
// ๐ซ This will fail!
viewerService.createProduct({ name: "Forbidden Book", price: 39.99, stock: 10, category: "books", isActive: true });
} catch (error) {
console.log("โ ๏ธ Authorization failed:", error.message);
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Dynamic Permissions
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Dynamic permission system
interface DynamicPermission {
resource: string;
action: string;
conditions: (user: any, resource: any) => boolean; // ๐ง Smart conditions
}
// ๐ช Context-aware authorization
class AdvancedAuthService {
private permissions: DynamicPermission[] = [];
// ๐ Add dynamic permission
addPermission(permission: DynamicPermission): void {
this.permissions.push(permission);
}
// ๐ฏ Check with context
canAccess(user: any, action: string, resource: any): boolean {
return this.permissions.some(permission =>
permission.action === action &&
permission.resource === resource.type &&
permission.conditions(user, resource)
);
}
}
// ๐ฎ Usage example
const advancedAuth = new AdvancedAuthService();
// ๐ Users can only edit their own posts
advancedAuth.addPermission({
resource: "post",
action: "edit",
conditions: (user, post) => user.id === post.authorId
});
// ๐ฅ Managers can edit posts in their department
advancedAuth.addPermission({
resource: "post",
action: "edit",
conditions: (user, post) => user.role === "manager" && user.department === post.department
});
๐๏ธ Advanced Topic 2: Hierarchical Roles
For the brave developers:
// ๐ Role hierarchy system
class RoleHierarchy {
private hierarchy = new Map<Role, Role[]>([
[Role.ADMIN, [Role.USER, Role.GUEST]], // ๐ Admin inherits from user and guest
[Role.USER, [Role.GUEST]] // ๐ค User inherits from guest
]);
// ๐ Get all inherited roles
getInheritedRoles(role: Role): Role[] {
const inherited = this.hierarchy.get(role) || [];
const allRoles = [role, ...inherited];
// ๐ Recursively get inherited roles
inherited.forEach(inheritedRole => {
const deeper = this.getInheritedRoles(inheritedRole);
allRoles.push(...deeper.filter(r => !allRoles.includes(r)));
});
return allRoles;
}
// โ
Check if role has permission through inheritance
hasPermissionThroughInheritance(userRole: Role, requiredRole: Role): boolean {
const inheritedRoles = this.getInheritedRoles(userRole);
return inheritedRoles.includes(requiredRole);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Hard-coded Permissions
// โ Wrong way - hard-coded everywhere!
function deleteUser(userId: string, currentUser: User) {
if (currentUser.role !== "admin") { // ๐ฅ Hard-coded!
throw new Error("Not allowed");
}
// Delete logic...
}
// โ
Correct way - centralized authorization!
class UserService {
constructor(private auth: AuthorizationService) {}
@requireRole([Role.ADMIN])
deleteUser(userId: string): void {
console.log(`๐๏ธ Deleted user: ${userId}`);
}
}
๐คฏ Pitfall 2: Forgetting to Check Authorization
// โ Dangerous - no authorization check!
async function sensitiveOperation(data: any) {
return await database.deleteAllData(); // ๐ฅ Anyone can do this!
}
// โ
Safe - always check first!
@requireRole([Role.SUPER_ADMIN])
async function sensitiveOperation(data: any) {
console.log("๐ก๏ธ Authorization passed, proceeding...");
return await database.deleteAllData(); // โ
Only super admins!
}
๐จ Pitfall 3: Role Information in Frontend
// โ Wrong - trusting the frontend!
// Frontend sends: { role: "admin" }
// Server believes it without verification ๐ฅ
// โ
Correct - server-side verification!
class SecureUserService {
async getCurrentUser(token: string): Promise<User> {
const decoded = jwt.verify(token, SECRET_KEY); // ๐ Verify token
return await database.users.findById(decoded.userId); // ๐ก๏ธ Get from DB
}
}
๐ ๏ธ Best Practices
- ๐ฏ Principle of Least Privilege: Give minimum permissions needed
- ๐ Centralized Authorization: Keep all permission logic in one place
- ๐ก๏ธ Server-side Verification: Never trust the client
- ๐ Secure Token Storage: Use HTTPOnly cookies or secure storage
- ๐ Audit Everything: Log all authorization decisions
- ๐ Regular Reviews: Periodically review user permissions
- ๐งช Test Authorization: Write tests for permission edge cases
๐งช Hands-On Exercise
๐ฏ Challenge: Build a School Management System
Create a type-safe school administration system:
๐ Requirements:
- ๐ Students can view their own grades and schedules
- ๐จโ๐ซ Teachers can manage their classes and grade students
- ๐ Principals can access everything
- ๐ Librarians can manage library resources
- ๐ฅ Parents can view their childโs information
- ๐ Sensitive information (disciplinary records) restricted to admin
๐ Bonus Points:
- Add grade-level restrictions (elementary vs high school)
- Implement subject-specific teacher permissions
- Create a parent-student relationship system
- Add audit logging for sensitive operations
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe school system!
enum SchoolRole {
STUDENT = "student",
TEACHER = "teacher",
PRINCIPAL = "principal",
LIBRARIAN = "librarian",
PARENT = "parent"
}
// ๐ค School user interface
interface SchoolUser {
id: string;
name: string;
role: SchoolRole;
gradeLevel?: number; // ๐ For students (K-12)
subjects?: string[]; // ๐ For teachers
childrenIds?: string[]; // ๐จโ๐ฉโ๐งโ๐ฆ For parents
}
// ๐ Student record
interface StudentRecord {
studentId: string;
grades: { subject: string; grade: string; teacherId: string }[];
schedule: { subject: string; time: string; teacherId: string }[];
disciplinaryRecords: string[]; // ๐ Sensitive
parentIds: string[];
}
// ๐ Library resource
interface LibraryResource {
id: string;
title: string;
type: "book" | "digital" | "equipment";
isAvailable: boolean;
borrowedBy?: string;
}
// ๐ก๏ธ School authorization service
class SchoolAuthService {
private currentUser: SchoolUser;
constructor(user: SchoolUser) {
this.currentUser = user;
}
// ๐ Can view student grades?
canViewGrades(studentRecord: StudentRecord): boolean {
switch (this.currentUser.role) {
case SchoolRole.STUDENT:
return studentRecord.studentId === this.currentUser.id;
case SchoolRole.TEACHER:
// ๐จโ๐ซ Can see grades for subjects they teach
return studentRecord.grades.some(grade =>
this.currentUser.subjects?.includes(grade.subject)
);
case SchoolRole.PRINCIPAL:
return true; // ๐ Full access
case SchoolRole.PARENT:
return studentRecord.parentIds.includes(this.currentUser.id);
default:
return false;
}
}
// โ๏ธ Can modify grades?
canModifyGrades(studentRecord: StudentRecord, subject: string): boolean {
switch (this.currentUser.role) {
case SchoolRole.TEACHER:
return this.currentUser.subjects?.includes(subject) ?? false;
case SchoolRole.PRINCIPAL:
return true; // ๐ Can override any grade
default:
return false;
}
}
// ๐ Can view disciplinary records?
canViewDisciplinaryRecords(studentRecord: StudentRecord): boolean {
switch (this.currentUser.role) {
case SchoolRole.PRINCIPAL:
return true; // ๐ Full access to sensitive records
case SchoolRole.TEACHER:
// ๐ค Only if they teach the student
return studentRecord.grades.some(grade =>
this.currentUser.subjects?.includes(grade.subject)
);
default:
return false; // ๐ซ Sensitive information
}
}
// ๐ Can manage library resources?
canManageLibrary(): boolean {
return this.currentUser.role === SchoolRole.LIBRARIAN ||
this.currentUser.role === SchoolRole.PRINCIPAL;
}
// ๐ Can borrow books?
canBorrowBooks(): boolean {
return [SchoolRole.STUDENT, SchoolRole.TEACHER].includes(this.currentUser.role);
}
}
// ๐ School management service
class SchoolService {
constructor(private auth: SchoolAuthService) {}
// ๐ Get student grades
getGrades(studentId: string): any {
const studentRecord: StudentRecord = {
studentId,
grades: [
{ subject: "Math", grade: "A", teacherId: "teacher001" },
{ subject: "Science", grade: "B+", teacherId: "teacher002" }
],
schedule: [],
disciplinaryRecords: ["Late to class on 2024-01-15"],
parentIds: ["parent001"]
};
if (!this.auth.canViewGrades(studentRecord)) {
throw new Error("๐ซ Access denied: Cannot view grades");
}
return studentRecord.grades;
}
// โ๏ธ Update grade
updateGrade(studentId: string, subject: string, newGrade: string): void {
const studentRecord: StudentRecord = {
studentId,
grades: [{ subject, grade: "B", teacherId: "teacher001" }],
schedule: [],
disciplinaryRecords: [],
parentIds: []
};
if (!this.auth.canModifyGrades(studentRecord, subject)) {
throw new Error(`๐ซ Access denied: Cannot modify ${subject} grades`);
}
console.log(`โ
Updated ${subject} grade to ${newGrade} for student ${studentId}`);
}
// ๐ Borrow book
borrowBook(bookId: string): void {
if (!this.auth.canBorrowBooks()) {
throw new Error("๐ซ Access denied: Cannot borrow books");
}
console.log(`๐ Book ${bookId} borrowed successfully`);
}
}
// ๐ฎ Test the system!
const student: SchoolUser = {
id: "student001",
name: "Emma Smith",
role: SchoolRole.STUDENT,
gradeLevel: 10
};
const teacher: SchoolUser = {
id: "teacher001",
name: "Mr. Johnson",
role: SchoolRole.TEACHER,
subjects: ["Math", "Physics"]
};
const parent: SchoolUser = {
id: "parent001",
name: "Mrs. Smith",
role: SchoolRole.PARENT,
childrenIds: ["student001"]
};
// ๐งช Test different permissions
const studentAuth = new SchoolAuthService(student);
const teacherAuth = new SchoolAuthService(teacher);
const parentAuth = new SchoolAuthService(parent);
const studentService = new SchoolService(studentAuth);
const teacherService = new SchoolService(teacherAuth);
const parentService = new SchoolService(parentAuth);
try {
// โ
These should work
console.log("๐ Student grades:", studentService.getGrades("student001"));
teacherService.updateGrade("student001", "Math", "A+");
studentService.borrowBook("book123");
// ๐ซ This should fail
studentService.updateGrade("student001", "Math", "A+");
} catch (error) {
console.log("โ ๏ธ Authorization failed:", error.message);
}
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create role-based authorization with confidence ๐ช
- โ Avoid common security mistakes that trip up beginners ๐ก๏ธ
- โ Apply RBAC patterns in real projects ๐ฏ
- โ Debug authorization issues like a pro ๐
- โ Build secure applications with TypeScript! ๐
Remember: Security is not a feature you add later - itโs a foundation you build on! ๐๏ธ
๐ค Next Steps
Congratulations! ๐ Youโve mastered role-based authorization!
Hereโs what to do next:
- ๐ป Practice with the school management exercise above
- ๐๏ธ Build a small project using RBAC (try a blog with admin/author/reader roles)
- ๐ Move on to our next tutorial: JWT Authentication Implementation
- ๐ Share your secure applications with others!
Remember: Every security expert was once a beginner. Keep coding, keep learning, and most importantly, keep your applications secure! ๐
Happy coding and stay secure! ๐๐โจ