Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on authorization patterns and access control! ๐ In this guide, weโll explore how to build secure, type-safe authorization systems that protect your applicationโs resources.
Youโll discover how proper authorization patterns can transform your TypeScript applications into secure fortresses ๐ฐ. Whether youโre building web applications ๐, APIs ๐ฅ๏ธ, or enterprise systems ๐, understanding authorization is essential for protecting user data and maintaining system integrity.
By the end of this tutorial, youโll feel confident implementing robust access control in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Authorization Patterns
๐ค What is Authorization?
Authorization is like being a bouncer at an exclusive club ๐ญ. Think of it as a security checkpoint that decides who gets access to what resources. While authentication asks โWho are you?โ, authorization asks โWhat are you allowed to do?โ ๐
In TypeScript terms, authorization is the process of determining if an authenticated user has permission to perform specific actions or access certain resources. This means you can:
- โจ Control access to sensitive data
- ๐ Implement role-based permissions
- ๐ก๏ธ Protect critical operations
๐ก Why Use Authorization Patterns?
Hereโs why developers prioritize authorization:
- Security ๐: Protect sensitive resources from unauthorized access
- Compliance ๐ป: Meet regulatory requirements (GDPR, HIPAA, etc.)
- User Experience ๐: Show users only what they can access
- Scalability ๐ง: Manage complex permission systems efficiently
Real-world example: Imagine building a hospital management system ๐ฅ. With proper authorization, doctors can access patient records, nurses can update vitals, but visitors can only see general information.
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Authorization!
type Role = "admin" | "user" | "guest";
// ๐จ Creating a simple user interface
interface User {
id: string; // ๐ค User identifier
name: string; // ๐ User's name
role: Role; // ๐ญ User's role
permissions?: string[]; // ๐ Optional specific permissions
}
// ๐ก๏ธ Basic permission check
const canAccess = (user: User, resource: string): boolean => {
// ๐ฏ Admins can access everything!
if (user.role === "admin") return true;
// ๐ Check specific permissions
return user.permissions?.includes(resource) || false;
};
๐ก Explanation: Notice how we use union types for roles and optional permissions for flexibility!
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Role-Based Access Control (RBAC)
const rolePermissions: Record<Role, string[]> = {
admin: ["read", "write", "delete", "manage_users"],
user: ["read", "write"],
guest: ["read"]
};
// ๐จ Pattern 2: Permission-based checks
type Permission = "read" | "write" | "delete" | "manage_users";
const hasPermission = (user: User, permission: Permission): boolean => {
const userPermissions = rolePermissions[user.role];
return userPermissions.includes(permission);
};
// ๐ Pattern 3: Resource-based authorization
interface Resource {
id: string;
ownerId: string;
visibility: "public" | "private" | "restricted";
}
const canAccessResource = (user: User, resource: Resource): boolean => {
// ๐ Public resources are accessible to all
if (resource.visibility === "public") return true;
// ๐ค Owners can always access their resources
if (resource.ownerId === user.id) return true;
// ๐ฏ Check role-based access
return user.role === "admin";
};
๐ก Practical Examples
๐ Example 1: E-Commerce Access Control
Letโs build something real:
// ๐๏ธ Define our order and product types
interface Product {
id: string;
name: string;
price: number;
status: "active" | "draft" | "archived";
}
interface Order {
id: string;
userId: string;
products: Product[];
total: number;
status: "pending" | "completed" | "cancelled";
}
// ๐ก๏ธ Authorization service
class AuthorizationService {
// ๐ Check if user can view order
canViewOrder(user: User, order: Order): boolean {
// ๐ค Users can view their own orders
if (order.userId === user.id) return true;
// ๐ฎ Admins and support can view all orders
return ["admin", "support"].includes(user.role);
}
// ๐ณ Check if user can process refund
canProcessRefund(user: User, order: Order): boolean {
// ๐ซ Only completed orders can be refunded
if (order.status !== "completed") return false;
// ๐ฐ Only admins and finance team can process refunds
return user.role === "admin" ||
user.permissions?.includes("process_refunds") || false;
}
// ๐ Check if user can modify product
canModifyProduct(user: User, product: Product): boolean {
// ๐ Archived products can't be modified
if (product.status === "archived") return false;
// โ๏ธ Admins and product managers can modify
return user.role === "admin" ||
user.permissions?.includes("manage_products") || false;
}
}
// ๐ฎ Let's use it!
const authService = new AuthorizationService();
const currentUser: User = {
id: "123",
name: "Sarah",
role: "user"
};
const myOrder: Order = {
id: "order-456",
userId: "123",
products: [],
total: 99.99,
status: "completed"
};
console.log(`Can view order: ${authService.canViewOrder(currentUser, myOrder) ? "โ
" : "โ"}`);
console.log(`Can process refund: ${authService.canProcessRefund(currentUser, myOrder) ? "โ
" : "โ"}`);
๐ฏ Try it yourself: Add a feature to check if users can apply discount codes based on their membership level!
๐ฎ Example 2: Multi-Tenant Application
Letโs make it more complex:
// ๐ข Multi-tenant authorization system
interface Tenant {
id: string;
name: string;
plan: "free" | "pro" | "enterprise";
}
interface TenantUser extends User {
tenantId: string;
tenantRole: "owner" | "admin" | "member" | "viewer";
}
// ๐ฏ Feature flags based on tenant plan
const featureAccess: Record<string, string[]> = {
free: ["basic_reports", "5_users"],
pro: ["basic_reports", "advanced_reports", "50_users", "api_access"],
enterprise: ["all_features", "unlimited_users", "custom_integrations"]
};
class TenantAuthorizationService {
private tenants: Map<string, Tenant> = new Map();
// ๐๏ธ Check if tenant has access to feature
tenantHasFeature(tenantId: string, feature: string): boolean {
const tenant = this.tenants.get(tenantId);
if (!tenant) return false;
const planFeatures = featureAccess[tenant.plan];
return planFeatures.includes(feature) ||
planFeatures.includes("all_features");
}
// ๐ค Check user permissions within tenant
canUserPerformAction(
user: TenantUser,
action: string,
resource: string
): boolean {
// ๐ซ First check if tenant has the feature
if (!this.tenantHasFeature(user.tenantId, resource)) {
console.log("๐ซ Tenant plan doesn't include this feature!");
return false;
}
// ๐ญ Check user's tenant role
const roleHierarchy = {
owner: 4,
admin: 3,
member: 2,
viewer: 1
};
const requiredLevel = this.getRequiredLevel(action);
return roleHierarchy[user.tenantRole] >= requiredLevel;
}
// ๐ Get required permission level
private getRequiredLevel(action: string): number {
const actionLevels: Record<string, number> = {
view: 1,
create: 2,
edit: 2,
delete: 3,
manage: 4
};
return actionLevels[action] || 4;
}
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Attribute-Based Access Control (ABAC)
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced attribute-based authorization
interface AuthContext {
user: User;
resource: any;
action: string;
environment: {
time: Date;
ipAddress: string;
location?: string;
};
}
// ๐ช Policy definition
type PolicyRule = (context: AuthContext) => boolean;
interface Policy {
name: string;
description: string;
rules: PolicyRule[];
effect: "allow" | "deny";
}
// ๐ Advanced policy engine
class PolicyEngine {
private policies: Policy[] = [];
// โ Add a policy
addPolicy(policy: Policy): void {
this.policies.push(policy);
console.log(`โจ Added policy: ${policy.name}`);
}
// ๐ Evaluate all policies
evaluate(context: AuthContext): boolean {
// ๐ Collect all applicable policies
const results = this.policies.map(policy => {
const allRulesPassed = policy.rules.every(rule => rule(context));
return { passed: allRulesPassed, effect: policy.effect };
});
// ๐ซ Deny takes precedence
const hasDeny = results.some(r => r.passed && r.effect === "deny");
if (hasDeny) return false;
// โ
At least one allow
return results.some(r => r.passed && r.effect === "allow");
}
}
// ๐ฎ Example policy
const workingHoursPolicy: Policy = {
name: "Working Hours Access",
description: "Allow access only during working hours",
effect: "allow",
rules: [
(ctx) => {
const hour = ctx.environment.time.getHours();
return hour >= 9 && hour <= 17; // ๐ 9 AM to 5 PM
},
(ctx) => ctx.user.role !== "guest" // ๐ซ No guests
]
};
๐๏ธ Advanced Topic 2: Dynamic Permission System
For the brave developers:
// ๐ Type-safe dynamic permissions
type PermissionScope = {
resource: string;
actions: string[];
conditions?: Record<string, any>;
};
// ๐ซ Permission builder with fluent API
class PermissionBuilder {
private scopes: PermissionScope[] = [];
// ๐ฏ Add resource permission
on(resource: string): this {
this.scopes.push({ resource, actions: [] });
return this;
}
// โ Allow specific actions
allow(...actions: string[]): this {
const current = this.scopes[this.scopes.length - 1];
if (current) {
current.actions.push(...actions);
}
return this;
}
// ๐ Add conditions
when(conditions: Record<string, any>): this {
const current = this.scopes[this.scopes.length - 1];
if (current) {
current.conditions = conditions;
}
return this;
}
// ๐๏ธ Build final permission set
build(): PermissionScope[] {
return this.scopes;
}
}
// ๐จ Usage example
const permissions = new PermissionBuilder()
.on("posts")
.allow("read", "write")
.when({ isOwner: true })
.on("comments")
.allow("read")
.allow("write")
.when({ isVerified: true })
.build();
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: The โAlways Adminโ Trap
// โ Wrong way - hardcoding admin checks everywhere!
const deletePost = (user: User, postId: string) => {
if (user.role === "admin") {
// Delete logic
}
// ๐ฅ What about post owners? Moderators?
};
// โ
Correct way - flexible permission system!
const deletePost = (user: User, post: Post) => {
const canDelete =
user.role === "admin" ||
post.authorId === user.id ||
user.permissions?.includes("moderate_posts");
if (canDelete) {
console.log("๐๏ธ Post deleted successfully!");
// Delete logic
}
};
๐คฏ Pitfall 2: Forgetting Default Deny
// โ Dangerous - undefined means access!
const hasAccess = (permissions?: string[], required: string): boolean => {
return permissions?.includes(required); // ๐ฅ Returns undefined if no permissions!
};
// โ
Safe - explicit default deny!
const hasAccess = (permissions?: string[], required: string): boolean => {
return permissions?.includes(required) || false; // ๐ก๏ธ Always returns boolean
};
// ๐ฏ Even better - fail closed pattern
const secureAccess = (user: User, resource: string): boolean => {
try {
// Check various permission sources
const hasRole = checkRolePermission(user, resource);
const hasExplicit = checkExplicitPermission(user, resource);
const hasDelegate = checkDelegatedPermission(user, resource);
return hasRole || hasExplicit || hasDelegate;
} catch (error) {
console.error("โ ๏ธ Authorization check failed:", error);
return false; // ๐ Fail closed!
}
};
๐ ๏ธ Best Practices
- ๐ฏ Principle of Least Privilege: Grant minimum necessary permissions
- ๐ Audit Everything: Log all authorization decisions
- ๐ก๏ธ Fail Closed: Default to deny when uncertain
- ๐จ Separation of Concerns: Keep auth logic separate from business logic
- โจ Type Safety: Use TypeScript to enforce permission contracts
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Document Management System
Create a type-safe document authorization system:
๐ Requirements:
- โ Documents have owners, collaborators, and viewers
- ๐ท๏ธ Support folders with inherited permissions
- ๐ค Implement sharing with external users
- ๐ Time-based access (temporary links)
- ๐จ Each permission level needs an emoji!
๐ Bonus Points:
- Add audit logging for all access attempts
- Implement permission delegation
- Create a permission conflict resolver
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe document system!
type AccessLevel = "owner" | "editor" | "viewer";
type ShareType = "permanent" | "temporary";
interface Document {
id: string;
title: string;
ownerId: string;
folderId?: string;
createdAt: Date;
emoji: string; // Every doc needs an emoji! ๐
}
interface Permission {
userId: string;
documentId: string;
level: AccessLevel;
grantedBy: string;
expiresAt?: Date;
}
interface Folder {
id: string;
name: string;
parentId?: string;
permissions: Permission[];
}
class DocumentAuthorizationSystem {
private permissions: Permission[] = [];
private folders: Map<string, Folder> = new Map();
private auditLog: Array<{
timestamp: Date;
userId: string;
action: string;
documentId: string;
allowed: boolean;
}> = [];
// ๐ Check document access
canAccessDocument(
userId: string,
document: Document,
requiredLevel: AccessLevel
): boolean {
const startTime = Date.now();
// ๐ค Owner always has access
if (document.ownerId === userId) {
this.logAccess(userId, "access", document.id, true);
return true;
}
// ๐ Check direct permissions
const permission = this.permissions.find(
p => p.userId === userId && p.documentId === document.id
);
if (permission) {
// โฐ Check expiration
if (permission.expiresAt && permission.expiresAt < new Date()) {
this.logAccess(userId, "access", document.id, false);
console.log("โฐ Permission expired!");
return false;
}
const allowed = this.hasRequiredLevel(permission.level, requiredLevel);
this.logAccess(userId, "access", document.id, allowed);
return allowed;
}
// ๐ Check folder permissions
if (document.folderId) {
const folderAccess = this.checkFolderAccess(userId, document.folderId, requiredLevel);
this.logAccess(userId, "access", document.id, folderAccess);
return folderAccess;
}
this.logAccess(userId, "access", document.id, false);
return false;
}
// ๐ฏ Share document
shareDocument(
ownerId: string,
documentId: string,
shareWithUserId: string,
level: AccessLevel,
shareType: ShareType,
durationHours?: number
): boolean {
// Verify owner
const doc = { id: documentId, ownerId } as Document;
if (!this.canAccessDocument(ownerId, doc, "owner")) {
console.log("๐ซ Only owners can share!");
return false;
}
const permission: Permission = {
userId: shareWithUserId,
documentId,
level,
grantedBy: ownerId,
expiresAt: shareType === "temporary" && durationHours
? new Date(Date.now() + durationHours * 60 * 60 * 1000)
: undefined
};
this.permissions.push(permission);
console.log(`โ
Shared ${this.getLevelEmoji(level)} access with user!`);
if (shareType === "temporary") {
console.log(`โฐ Access expires in ${durationHours} hours`);
}
return true;
}
// ๐ Get access level hierarchy
private hasRequiredLevel(userLevel: AccessLevel, required: AccessLevel): boolean {
const levels: Record<AccessLevel, number> = {
owner: 3,
editor: 2,
viewer: 1
};
return levels[userLevel] >= levels[required];
}
// ๐จ Get emoji for access level
private getLevelEmoji(level: AccessLevel): string {
const emojis: Record<AccessLevel, string> = {
owner: "๐",
editor: "โ๏ธ",
viewer: "๐๏ธ"
};
return emojis[level];
}
// ๐ Check folder permissions (recursive)
private checkFolderAccess(
userId: string,
folderId: string,
requiredLevel: AccessLevel
): boolean {
const folder = this.folders.get(folderId);
if (!folder) return false;
// Check folder permissions
const folderPerm = folder.permissions.find(p => p.userId === userId);
if (folderPerm && this.hasRequiredLevel(folderPerm.level, requiredLevel)) {
return true;
}
// Check parent folder
if (folder.parentId) {
return this.checkFolderAccess(userId, folder.parentId, requiredLevel);
}
return false;
}
// ๐ Audit logging
private logAccess(
userId: string,
action: string,
documentId: string,
allowed: boolean
): void {
this.auditLog.push({
timestamp: new Date(),
userId,
action,
documentId,
allowed
});
}
// ๐ Get audit report
getAuditReport(): void {
console.log("๐ Access Audit Report:");
console.log(` ๐ Total attempts: ${this.auditLog.length}`);
console.log(` โ
Allowed: ${this.auditLog.filter(l => l.allowed).length}`);
console.log(` โ Denied: ${this.auditLog.filter(l => !l.allowed).length}`);
}
}
// ๐ฎ Test it out!
const docAuth = new DocumentAuthorizationSystem();
const myDoc: Document = {
id: "doc-123",
title: "Secret Plans",
ownerId: "user-1",
createdAt: new Date(),
emoji: "๐"
};
// Share with temporary access
docAuth.shareDocument("user-1", "doc-123", "user-2", "viewer", "temporary", 24);
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Implement RBAC with confidence ๐ช
- โ Build secure access control systems ๐ก๏ธ
- โ Avoid common security pitfalls ๐ฏ
- โ Create flexible permission models ๐
- โ Design type-safe authorization with TypeScript! ๐
Remember: Security is not a feature, itโs a foundation! Always think โdeny by defaultโ and grant permissions explicitly. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered authorization patterns and access control!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build an authorization system for your project
- ๐ Move on to our next tutorial: Session Management Security
- ๐ Share your secure implementations with others!
Remember: Every security expert started by learning the basics. Keep building secure systems, keep learning, and most importantly, keep your usersโ data safe! ๐
Happy coding! ๐๐โจ