Authorization
Authorization is the process of determining what an authenticated user is allowed to do. While authentication verifies "who you are," authorization determines "what you can do."
Authentication vs Authorization
javascript// Authentication: Verify user identity const user = await login('[email protected]', 'password'); // Authorization: Check permissions if (user.role === 'admin') { // User can delete posts await deletePost(postId); } else { throw new Error('Forbidden: Admin access required'); }
Authorization Models
1. Role-Based Access Control (RBAC)
Users are assigned roles, and roles have permissions.
javascript// User roles const roles = { admin: ['read', 'write', 'delete', 'manage_users'], editor: ['read', 'write'], viewer: ['read'] }; // Check permission function hasPermission(user, permission) { const userPermissions = roles[user.role] || []; return userPermissions.includes(permission); } // Usage if (hasPermission(user, 'delete')) { await deletePost(postId); } // Middleware function requirePermission(permission) { return (req, res, next) => { if (!hasPermission(req.user, permission)) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } app.delete('/posts/:id', requirePermission('delete'), deletePostHandler);
2. Attribute-Based Access Control (ABAC)
Access is based on attributes of the user, resource, and environment.
javascriptfunction canEdit(user, post) { // Owner can edit if (post.authorId === user.id) { return true; } // Admins can edit if (user.role === 'admin') { return true; } // Editors can edit if post is not published if (user.role === 'editor' && !post.published) { return true; } return false; } // Usage if (canEdit(req.user, post)) { await updatePost(postId, updates); } else { res.status(403).json({ error: 'Not authorized to edit this post' }); }
3. Access Control Lists (ACL)
Specific permissions for individual users on specific resources.
javascriptconst acl = { 'post-123': { 'user-1': ['read', 'write'], 'user-2': ['read'] } }; function hasAccess(userId, resourceId, action) { const permissions = acl[resourceId]?.[userId] || []; return permissions.includes(action); } // Usage if (hasAccess(user.id, 'post-123', 'write')) { await updatePost('post-123', updates); }
Implementation Examples
Express Middleware
javascript// Role check middleware function requireRole(...allowedRoles) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Not authenticated' }); } if (!allowedRoles.includes(req.user.role)) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } // Usage app.get('/admin', requireRole('admin'), adminHandler); app.get('/dashboard', requireRole('admin', 'editor'), dashboardHandler);
Resource Ownership
javascriptasync function requireOwnership(req, res, next) { const post = await db.posts.findById(req.params.id); if (!post) { return res.status(404).json({ error: 'Not found' }); } if (post.authorId !== req.user.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Not authorized' }); } req.post = post; next(); } app.put('/posts/:id', requireOwnership, updatePostHandler); app.delete('/posts/:id', requireOwnership, deletePostHandler);
React Component Authorization
jsx// Higher-order component function withAuthorization(allowedRoles) { return function(Component) { return function AuthorizedComponent(props) { const { user } = useAuth(); if (!user) { return <Navigate to="/login" />; } if (!allowedRoles.includes(user.role)) { return <div>Access denied</div>; } return <Component {...props} />; }; }; } // Usage const AdminPanel = withAuthorization(['admin'])(AdminPanelComponent); // Hook-based approach function useAuthorization(requiredPermission) { const { user } = useAuth(); const hasPermission = useMemo(() => { if (!user) return false; const permissions = roles[user.role] || []; return permissions.includes(requiredPermission); }, [user, requiredPermission]); return hasPermission; } // Usage in component function DeleteButton({ postId }) { const canDelete = useAuthorization('delete'); if (!canDelete) return null; return ( <button onClick={() => deletePost(postId)}> Delete </button> ); }
Next.js Authorization
javascript// API Route export default async function handler(req, res) { const session = await getSession(req); if (!session) { return res.status(401).json({ error: 'Not authenticated' }); } if (session.user.role !== 'admin') { return res.status(403).json({ error: 'Forbidden' }); } // Handle request } // Server Component async function AdminPage() { const session = await getServerSession(); if (!session || session.user.role !== 'admin') { redirect('/login'); } return <AdminDashboard />; } // Middleware export function middleware(request) { const session = getSession(request); if (!session) { return NextResponse.redirect(new URL('/login', request.url)); } if (session.user.role !== 'admin') { return NextResponse.redirect(new URL('/forbidden', request.url)); } return NextResponse.next(); } export const config = { matcher: '/admin/:path*' };
Advanced Patterns
Permission-Based UI
jsxfunction PermissionGate({ permission, children, fallback = null }) { const { user } = useAuth(); const hasPermission = useMemo(() => { if (!user) return false; const permissions = roles[user.role] || []; return permissions.includes(permission); }, [user, permission]); return hasPermission ? children : fallback; } // Usage function PostActions({ post }) { return ( <div> <PermissionGate permission="write"> <button>Edit</button> </PermissionGate> <PermissionGate permission="delete"> <button>Delete</button> </PermissionGate> <PermissionGate permission="publish" fallback={<span>Cannot publish</span>} > <button>Publish</button> </PermissionGate> </div> ); }
Dynamic Permissions
javascript// Database-driven permissions async function getUserPermissions(userId) { const userRoles = await db.userRoles.find({ userId }); const roleIds = userRoles.map(r => r.roleId); const permissions = await db.rolePermissions.find({ roleId: { $in: roleIds } }); return permissions.map(p => p.permission); } // Check permission async function authorize(userId, permission) { const permissions = await getUserPermissions(userId); return permissions.includes(permission); } // Middleware async function requirePermission(permission) { return async (req, res, next) => { const hasPermission = await authorize(req.user.id, permission); if (!hasPermission) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; }
Hierarchical Roles
javascriptconst roleHierarchy = { admin: ['admin', 'editor', 'viewer'], editor: ['editor', 'viewer'], viewer: ['viewer'] }; function hasRole(userRole, requiredRole) { const allowedRoles = roleHierarchy[userRole] || []; return allowedRoles.includes(requiredRole); } // Admin can access editor and viewer routes if (hasRole(user.role, 'editor')) { // Allow access }
Context-Based Authorization
javascriptfunction canAccessResource(user, resource, context) { // Time-based const now = new Date(); if (resource.accessSchedule) { if (now < resource.accessStart || now > resource.accessEnd) { return false; } } // Location-based if (resource.allowedRegions) { if (!resource.allowedRegions.includes(context.region)) { return false; } } // Device-based if (resource.allowedDevices) { if (!resource.allowedDevices.includes(context.deviceType)) { return false; } } // Role-based return hasRole(user.role, resource.requiredRole); }
Common Patterns
Combine Multiple Checks
javascriptfunction canEditPost(user, post) { // Must be authenticated if (!user) return false; // Admin can edit anything if (user.role === 'admin') return true; // Owner can edit their own posts if (post.authorId === user.id) return true; // Editors can edit drafts if (user.role === 'editor' && post.status === 'draft') return true; return false; }
Scope-Based Authorization
javascript// API scopes (OAuth) const scopes = { 'read:posts': 'Read posts', 'write:posts': 'Create and edit posts', 'delete:posts': 'Delete posts', 'admin': 'Full access' }; function hasScope(userScopes, requiredScope) { return userScopes.includes(requiredScope) || userScopes.includes('admin'); } // Check in API if (!hasScope(req.user.scopes, 'delete:posts')) { return res.status(403).json({ error: 'Insufficient scope' }); }
Authorization Libraries
- CASL: Isomorphic authorization library
- Casbin: Authorization library supporting ACL, RBAC, ABAC
- AccessControl: RBAC and ABAC library for Node.js
- Permit.io: Authorization as a service
- Oso: Policy engine for authorization
Example with CASL
javascriptimport { defineAbility } from '@casl/ability'; function defineAbilitiesFor(user) { return defineAbility((can, cannot) => { if (user.role === 'admin') { can('manage', 'all'); } else if (user.role === 'editor') { can('read', 'Post'); can('create', 'Post'); can('update', 'Post', { authorId: user.id }); } else { can('read', 'Post'); } }); } // Usage const ability = defineAbilitiesFor(user); if (ability.can('update', post)) { await updatePost(post.id, updates); }
Best Practices
- Implement least privilege principle
- Always verify authorization on the server
- Don't rely solely on client-side authorization
- Log authorization failures for security monitoring
- Use middleware/guards for consistent checks
- Test authorization logic thoroughly
- Separate authorization from business logic
- Cache permissions when appropriate
- Provide clear error messages (without leaking info)
- Regularly audit and review permissions
Common Mistakes
javascript// Bad: Only client-side check if (user.role === 'admin') { <button onClick={deleteUser}>Delete</button> } // Good: Check on both client and server // Client (UI) {user.role === 'admin' && <button onClick={deleteUser}>Delete</button>} // Server (API) app.delete('/users/:id', (req, res) => { if (req.user.role !== 'admin') { return res.status(403).json({ error: 'Forbidden' }); } // Delete user });