Authentication verifies identity; authorization controls access. Here's how to implement flexible, secure authorization in your applications.
RBAC Basics#
1// Define roles and permissions
2const roles = {
3 admin: {
4 permissions: ['*'], // All permissions
5 },
6 editor: {
7 permissions: [
8 'posts:read',
9 'posts:create',
10 'posts:update',
11 'posts:delete',
12 'comments:read',
13 'comments:moderate',
14 ],
15 },
16 author: {
17 permissions: [
18 'posts:read',
19 'posts:create',
20 'posts:update:own',
21 'comments:read',
22 'comments:create',
23 ],
24 },
25 viewer: {
26 permissions: ['posts:read', 'comments:read'],
27 },
28};
29
30// Check permission
31function hasPermission(
32 userRole: string,
33 permission: string
34): boolean {
35 const role = roles[userRole];
36 if (!role) return false;
37
38 return (
39 role.permissions.includes('*') ||
40 role.permissions.includes(permission)
41 );
42}Database Schema#
1// schema.prisma
2model User {
3 id String @id @default(cuid())
4 email String @unique
5 roles UserRole[]
6}
7
8model Role {
9 id String @id @default(cuid())
10 name String @unique
11 permissions Permission[]
12 users UserRole[]
13}
14
15model Permission {
16 id String @id @default(cuid())
17 name String @unique // e.g., "posts:create"
18 roles Role[]
19}
20
21model UserRole {
22 user User @relation(fields: [userId], references: [id])
23 userId String
24 role Role @relation(fields: [roleId], references: [id])
25 roleId String
26 assignedAt DateTime @default(now())
27
28 @@id([userId, roleId])
29}Permission Checking Service#
1class AuthorizationService {
2 private permissionCache: Map<string, Set<string>> = new Map();
3
4 async getUserPermissions(userId: string): Promise<Set<string>> {
5 // Check cache
6 const cached = this.permissionCache.get(userId);
7 if (cached) return cached;
8
9 // Fetch from database
10 const user = await prisma.user.findUnique({
11 where: { id: userId },
12 include: {
13 roles: {
14 include: {
15 role: {
16 include: {
17 permissions: true,
18 },
19 },
20 },
21 },
22 },
23 });
24
25 const permissions = new Set<string>();
26
27 user?.roles.forEach((userRole) => {
28 userRole.role.permissions.forEach((permission) => {
29 permissions.add(permission.name);
30 });
31 });
32
33 // Cache for 5 minutes
34 this.permissionCache.set(userId, permissions);
35 setTimeout(() => this.permissionCache.delete(userId), 5 * 60 * 1000);
36
37 return permissions;
38 }
39
40 async can(
41 userId: string,
42 permission: string,
43 resource?: any
44 ): Promise<boolean> {
45 const permissions = await this.getUserPermissions(userId);
46
47 // Check exact permission
48 if (permissions.has(permission)) return true;
49
50 // Check wildcard
51 if (permissions.has('*')) return true;
52
53 // Check resource-level permission (e.g., posts:update:own)
54 const [resource_type, action] = permission.split(':');
55 if (permissions.has(`${resource_type}:${action}:own`)) {
56 return resource?.userId === userId;
57 }
58
59 return false;
60 }
61
62 invalidateCache(userId: string) {
63 this.permissionCache.delete(userId);
64 }
65}
66
67const authz = new AuthorizationService();Express Middleware#
1// Permission middleware
2function requirePermission(permission: string) {
3 return async (req: Request, res: Response, next: NextFunction) => {
4 const userId = req.user?.id;
5
6 if (!userId) {
7 return res.status(401).json({ error: 'Authentication required' });
8 }
9
10 const allowed = await authz.can(userId, permission);
11
12 if (!allowed) {
13 return res.status(403).json({ error: 'Permission denied' });
14 }
15
16 next();
17 };
18}
19
20// Resource-level permission
21function requireResourcePermission(
22 permission: string,
23 getResource: (req: Request) => Promise<any>
24) {
25 return async (req: Request, res: Response, next: NextFunction) => {
26 const userId = req.user?.id;
27
28 if (!userId) {
29 return res.status(401).json({ error: 'Authentication required' });
30 }
31
32 const resource = await getResource(req);
33
34 if (!resource) {
35 return res.status(404).json({ error: 'Resource not found' });
36 }
37
38 const allowed = await authz.can(userId, permission, resource);
39
40 if (!allowed) {
41 return res.status(403).json({ error: 'Permission denied' });
42 }
43
44 req.resource = resource;
45 next();
46 };
47}
48
49// Usage
50app.get('/posts', requirePermission('posts:read'), getPosts);
51
52app.put(
53 '/posts/:id',
54 requireResourcePermission('posts:update', async (req) => {
55 return prisma.post.findUnique({ where: { id: req.params.id } });
56 }),
57 updatePost
58);ABAC (Attribute-Based)#
1// More flexible than RBAC
2interface Policy {
3 resource: string;
4 action: string;
5 condition: (context: PolicyContext) => boolean;
6}
7
8interface PolicyContext {
9 user: User;
10 resource: any;
11 environment: {
12 time: Date;
13 ip: string;
14 };
15}
16
17const policies: Policy[] = [
18 {
19 resource: 'posts',
20 action: 'update',
21 condition: ({ user, resource }) => {
22 return user.id === resource.authorId || user.role === 'admin';
23 },
24 },
25 {
26 resource: 'reports',
27 action: 'download',
28 condition: ({ user, environment }) => {
29 // Only during business hours
30 const hour = environment.time.getHours();
31 return user.role === 'analyst' && hour >= 9 && hour <= 17;
32 },
33 },
34 {
35 resource: 'admin',
36 action: '*',
37 condition: ({ user, environment }) => {
38 // Admin access only from office IP
39 return user.role === 'admin' &&
40 environment.ip.startsWith('10.0.');
41 },
42 },
43];
44
45class PolicyEngine {
46 evaluate(
47 resource: string,
48 action: string,
49 context: PolicyContext
50 ): boolean {
51 const matchingPolicies = policies.filter(
52 (p) =>
53 (p.resource === resource || p.resource === '*') &&
54 (p.action === action || p.action === '*')
55 );
56
57 return matchingPolicies.some((policy) => policy.condition(context));
58 }
59}Role Hierarchy#
1// Roles inherit from parent roles
2const roleHierarchy: Record<string, string[]> = {
3 admin: ['editor', 'author', 'viewer'],
4 editor: ['author', 'viewer'],
5 author: ['viewer'],
6 viewer: [],
7};
8
9function getEffectiveRoles(role: string): string[] {
10 const roles = [role];
11 const inherited = roleHierarchy[role] || [];
12
13 for (const inheritedRole of inherited) {
14 roles.push(...getEffectiveRoles(inheritedRole));
15 }
16
17 return [...new Set(roles)];
18}
19
20// admin -> ['admin', 'editor', 'author', 'viewer']Frontend Integration#
1// React context for permissions
2const AuthContext = createContext<{
3 user: User | null;
4 permissions: string[];
5 can: (permission: string) => boolean;
6}>({ user: null, permissions: [], can: () => false });
7
8function AuthProvider({ children }: { children: React.ReactNode }) {
9 const [user, setUser] = useState<User | null>(null);
10 const [permissions, setPermissions] = useState<string[]>([]);
11
12 useEffect(() => {
13 fetchCurrentUser().then(({ user, permissions }) => {
14 setUser(user);
15 setPermissions(permissions);
16 });
17 }, []);
18
19 const can = (permission: string) => {
20 return permissions.includes(permission) || permissions.includes('*');
21 };
22
23 return (
24 <AuthContext.Provider value={{ user, permissions, can }}>
25 {children}
26 </AuthContext.Provider>
27 );
28}
29
30// Permission component
31function Can({
32 permission,
33 children,
34 fallback = null,
35}: {
36 permission: string;
37 children: React.ReactNode;
38 fallback?: React.ReactNode;
39}) {
40 const { can } = useContext(AuthContext);
41 return can(permission) ? <>{children}</> : <>{fallback}</>;
42}
43
44// Usage
45<Can permission="posts:create">
46 <Button>Create Post</Button>
47</Can>
48
49<Can permission="admin:access" fallback={<AccessDenied />}>
50 <AdminPanel />
51</Can>Best Practices#
DO:
✓ Deny by default
✓ Check permissions on both frontend and backend
✓ Cache permissions appropriately
✓ Log access attempts
✓ Use principle of least privilege
✓ Audit permission changes
DON'T:
✗ Trust client-side permission checks alone
✗ Hardcode permissions in code
✗ Give admin access by default
✗ Forget to invalidate cache on changes
Conclusion#
Start with simple RBAC, evolve to ABAC if needed. Always enforce authorization on the server, cache permissions for performance, and audit access for security.
The best authorization system is one that's strict by default and flexible when needed.