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.

javascript
function 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.

javascript
const 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

javascript
async 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

jsx
function 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

javascript
const 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

javascript
function 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

javascript
function 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

javascript
import { 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
});

Learn More