RBAC vs ABAC: Understanding Modern Authorization Models

5 min read
Nov 20, 2024

Welcome to Permission Systems That Scale

In this blog, you'll learn how to design robust, maintainable authorization systems that can handle real-world business requirements. By the end, you'll understand the strengths, limitations, and trade-offs of different authorization models, along with how to implement them from scratch.

What You'll Learn

Throughout this workshop, you'll learn how to:

  • Implement Role-Based Access Control (RBAC) using clean and type-safe code.
  • Build Attribute-Based Access Control (ABAC) for complex and dynamic authorization scenarios.
  • Understand how popular authorization libraries like CASL and Casbin manage permissions.
  • Organize permission logic in a way that is scalable, maintainable, and easy to extend.

Authentication vs Authorization

Before exploring permission systems like RBAC and ABAC, it's important to understand two fundamental security concepts that are often confused: authentication and authorization.

AuthenticationAuthorization
Who are you?What are you allowed to do?
Verifies a user's identityDetermines a user's permissions
Happens before authorizationHappens after authentication
Example: Logging in with an email and passwordExample: Checking whether a user can delete a document

Think of it like entering an office building.

  • Authentication is showing your employee ID at the entrance to prove your identity.
  • Authorization is determining which floors or rooms your ID card allows you to access.

Why Authorization Is More Challenging

Authentication is usually handled by authentication providers such as Auth.js, Clerk, Firebase Authentication, or Supabase Auth. Authorization, however, is unique to every application because it depends entirely on business requirements.

1. Every Application Has Different Business Rules

Permission rules are rarely the same between applications.

For example:

  • Editors can update documents only within their own department.
  • Authors can publish their own drafts but cannot publish someone else's content.
  • Administrators can manage users but cannot delete archived records.

Unlike authentication, these rules cannot simply be copied from another project. They must be designed specifically for your application's business logic.

2. Authorization Exists Throughout the Application

Permission checks are not limited to a single location. They appear across the entire application.

LayerExample Permission Check
APICan this user call this endpoint?
DatabaseWhich records should this user be allowed to query?
UIShould this button or menu item be visible?
Business LogicIs this operation allowed for the current user?

As an application grows, authorization becomes one of the most frequently used parts of the codebase.

3. Business Requirements Continuously Change

Authorization rules rarely stay the same.

Over time, your application may require changes such as:

  • Introducing new user roles.
  • Restricting access after a certain period.
  • Allowing managers to approve requests across departments.
  • Supporting multiple organizations or tenants.

A poorly designed permission system quickly becomes difficult to maintain and slows down future development.

Common Problems Caused by Poor Authorization

An ineffective authorization system can create serious security and usability issues.

ProblemResult
Overly permissive accessSensitive data may be exposed or modified by unauthorized users.
Overly restrictive accessLegitimate users cannot perform actions they should be allowed to do.
Inconsistent permission checksSome endpoints remain protected while others accidentally expose data.
Scattered permission logicEvery feature becomes harder to develop, test, and maintain.

The Core Question of Authorization

Once a user has successfully logged in, authentication is complete.

The next question becomes:

Given this authenticated user, what actions should they be allowed to perform?

A permission system exists to answer that question consistently and securely.

Characteristics of a Good Permission System

A well-designed authorization system should follow several important principles.

PrincipleDescription
Protect all sensitive dataEvery request should be validated before data is read or modified.
Single Source of TruthPermission rules should be centralized instead of scattered throughout the codebase.
Automatic EnforcementAuthorization should be applied automatically rather than relying on developers to remember adding permission checks.
Consistent BehaviorThe same permission rules should produce identical results on both the frontend and backend.
Never Trust the ClientClient-side permission checks improve user experience only. All security decisions must always be enforced on the server.
Fail ClosedIf a permission cannot be verified, access should be denied by default instead of being granted.

Best Practice

Client-side authorization is only for improving the user experience, such as hiding buttons or disabling actions. The server must always perform the final authorization check before processing a request.

Understanding the Data Model

Before implementing any permission system, it's important to understand what we're protecting.

In this example application, there are three main entities:

  • Users – People who use the application.
  • Projects – Containers that organize documents.
  • Documents – The actual content users create and manage.

Each entity has its own responsibilities and relationships.

EntityPurpose
UserRepresents an authenticated person with a specific role and department.
ProjectGroups related documents together and can belong to a department.
DocumentStores the actual content created and edited by users.

The permission system decides which users can perform actions such as viewing, creating, editing, or deleting these resources.

The First Problem I Noticed

When I first looked at the application, the permission system worked, but the authorization logic was spread across different parts of the codebase.

For example, permission checks existed inside:

  • Page components
  • API actions
  • Database access logic
  • UI components

This made the code difficult to maintain because the same permission rules were written multiple times.

Why Is This a Problem?

When authorization logic is duplicated, it creates several issues.

ProblemImpact
Duplicate permission checksThe same rule has to be updated in multiple places.
Missing authorizationSome pages or APIs may accidentally skip security checks.
Inconsistent logicDifferent developers may implement the same permission differently.

Imagine updating a rule so that Managers can edit projects. If the permission check exists in ten different files, forgetting to update just one of them can introduce security bugs.

A Better Approach

Instead of checking permissions everywhere, it's much cleaner to centralize authorization in a dedicated service layer.

Pages
   │
   ▼
Services (Authorization + Business Logic)
   │
   ▼
Database

With this approach:

  • Pages focus on rendering the UI.
  • Services handle authorization and business rules.
  • The data layer is responsible only for database operations.

This keeps the application easier to understand, test, and maintain.

Key Takeaways

  • Keep authorization logic in one place.
  • Never trust client-side permission checks.
  • Always validate permissions on the server.
  • Use descriptive helper functions instead of repeating role comparisons.
  • Design permission systems that can evolve as business requirements change.

These principles make permission systems easier to maintain while reducing the chances of introducing security vulnerabilities.

What is Role-Based Access Control (RBAC)?

Role-Based Access Control (RBAC) is one of the most common authorization models used in modern applications.

The idea behind RBAC is simple:

Instead of assigning permissions directly to users, permissions are assigned to roles, and users are assigned those roles.

User → Role → Permissions

For example, instead of checking whether a user is an administrator throughout your application, you define the administrator's permissions once and reuse them everywhere.

RoleCommon Permissions
ViewerRead data
AuthorCreate and edit their own content
EditorManage and update content
AdminFull access

Why RBAC Is Better Than Inline Permission Checks

When I first started learning authorization, I noticed that many applications directly compare user roles throughout the codebase.

if (user.role === "admin") {
  // Allow access
}

At first, this seems simple, but as the application grows, the same condition gets repeated in many different files.

A better approach is to centralize permissions.

// Define your permissions
type Permission =
  | "document:create"
  | "document:read"
  | "document:update"
  | "document:delete"
  | "project:create"
  | "project:read"
  | "project:update"
  | "project:delete"

// Map roles to permissions
const permissionsByRole: Record<UserRole, Permission[]> = {
  admin: [
    "document:create",
    "document:read",
    "document:update",
    "document:delete",
    "project:create",
    "project:read",
    "project:update",
    "project:delete",
  ],
  editor: ["document:read", "document:update"],
  viewer: ["document:read"],
}

// Check if a user can perform an action
function can(user: { role: UserRole }, permission: Permission) {
  return permissionsByRole[user.role].includes(permission)
}

Usage is clean and readable:

if (can(user, "document:create")) {
  // Show create button
}

if (!can(user, "project:delete")) {
  throw new AuthorizationError()
}

if (can(user, "document:create")) {
  // Allow access
}

This small change makes the code much easier to read and maintain.

Benefits of RBAC

Some of the biggest advantages of RBAC include:

  • Centralized permission management
  • Cleaner and more readable code
  • Easier maintenance
  • Reduced duplicate permission checks
  • Better type safety when using TypeScript

Instead of searching the entire project for role comparisons, permission changes only need to be updated in one place.

Where RBAC Starts to Struggle

RBAC works really well when permissions depend only on a user's role.

However, real-world applications often require additional conditions.

For example:

  • Only the creator can edit their own draft.
  • Locked documents cannot be edited.
  • Users can only access resources in their own department.
  • Access depends on the current time or location.

These rules depend on more than just the user's role.

For example:

User is Author
        +
Owns the Document
        +
Document is Draft
        +
Document is Not Locked
        ↓
Can Edit

Trying to represent these rules using only roles quickly becomes difficult.

Permission Explosion

As business requirements grow, RBAC permissions often become more specific.

Instead of simple permissions like:

document:read
document:update

they slowly evolve into:

document:update:own
document:update:own:draft
document:update:own:draft:unlocked

This is commonly known as permission explosion.

Managing dozens of highly specific permissions makes the system harder to understand and maintain.

My Takeaway

RBAC is an excellent choice for applications with relatively simple authorization requirements.

If permissions mainly depend on a user's role, RBAC provides a clean and maintainable solution.

However, once permissions depend on ownership, resource attributes, departments, document status, or other contextual information, RBAC starts becoming difficult to scale.

What is Attribute-Based Access Control (ABAC)?

While learning about permission systems, I discovered that Role-Based Access Control (RBAC) isn't always enough for real-world applications.

Sometimes, permissions depend on much more than a user's role.

For example:

  • Is the user the creator of the document?
  • Is the document still a draft?
  • Is the document locked?
  • Does the user belong to the same department?

These types of rules are difficult to express using only roles. This is where Attribute-Based Access Control (ABAC) becomes useful.

How ABAC Works

Instead of asking:

"Does this role have permission?"

ABAC asks:

"Do the attributes of this request satisfy the access policy?"

An authorization decision is made by evaluating different attributes.

AttributeExamples
User (Subject)Role, ID, Department
ResourceOwner, Status, Locked State
ActionRead, Create, Update, Delete
EnvironmentTime, Location, Device

RBAC vs ABAC

RBACABAC
Permissions are based on user roles.Permissions are based on multiple attributes.
Simple to implement.More flexible and scalable.
Best for straightforward permission models.Best for applications with complex business rules.

A Practical Example

Suppose an author should only be able to edit their own draft documents.

With RBAC, this quickly becomes difficult because the decision depends on multiple conditions.

ABAC can express the same rule naturally:

User Role = Author
        +
Document Owner = Current User
        +
Document Status = Draft
        +
Document Locked = No
        ↓
Allow Edit

Instead of creating dozens of specialized permissions, ABAC simply evaluates the required attributes.

Why ABAC Scales Better

As an application grows, business rules become more complex.

For example:

  • Managers can only access projects in their department.
  • Employees can edit documents only during business hours.
  • Confidential documents require a higher security clearance.
  • Only the creator can modify an unpublished document.

These requirements are difficult to model with RBAC alone, but they fit naturally into ABAC because authorization is based on policies rather than predefined roles.

My Takeaway

RBAC is an excellent choice when permissions depend mainly on user roles.

However, once authorization starts depending on ownership, document state, departments, or other contextual information, ABAC provides a much cleaner and more scalable solution.

Choosing between RBAC and ABAC isn't about which one is better—it's about selecting the right model for your application's complexity.

Taking ABAC One Step Further

One thing I found interesting while learning ABAC is that it isn't limited to deciding whether a user can perform an action. It can also control what data users can see, what they can modify, and even when they are allowed to perform an action.

This makes ABAC much more powerful than traditional RBAC.

Field-Level Permissions

Sometimes users should have access to a resource but not every field inside it.

For example, every user may be able to view a document, but only administrators should see certain metadata.

FieldAdminEditorAuthorViewer
Title
Content
Created At
Updated At

Similarly, write permissions can also be restricted.

For example, an author may update the document content but shouldn't be able to publish or lock the document.

This level of control is difficult to achieve with RBAC alone.

Environment-Based Permissions

Another feature I learned about is that authorization doesn't always depend on the user or the resource.

Sometimes the environment also matters.

Examples include:

  • Allow editing only during business hours.
  • Restrict access based on location.
  • Allow specific actions only from trusted devices.
  • Block updates during weekends.

These rules make permissions more context-aware and are one of the biggest strengths of ABAC.

Automatic Data Filtering

A good permission system shouldn't only decide whether a user has access—it should also ensure that users only receive data they are allowed to see.

Instead of fetching every record and filtering it manually, authorization rules can be applied directly to database queries.

This approach provides several benefits:

  • Improved security
  • Better performance
  • Less duplicated code
  • Lower risk of accidentally exposing sensitive data

Is ABAC Always the Right Choice?

Although ABAC is incredibly flexible, it also introduces additional complexity.

As more resources, attributes, and policies are added, the authorization logic becomes harder to understand and maintain.

For smaller applications, a simple RBAC implementation is often enough.

For larger applications with ownership rules, departments, document states, or contextual permissions, ABAC usually provides a cleaner and more scalable solution.

My Takeaway

The biggest lesson I learned is that there isn't a single permission model that fits every application.

  • RBAC is simple, easy to implement, and works well for straightforward role-based permissions.
  • ABAC offers much greater flexibility by making authorization decisions based on attributes instead of roles.

Choosing the right approach depends on your application's complexity rather than simply picking the more advanced solution.

Using CASL Instead of Building Everything Yourself

While building a custom ABAC system is a great way to understand how authorization works, maintaining it in a production application can become challenging.

Instead of implementing every feature yourself, you can use a mature authorization library like CASL.

CASL provides many advanced features out of the box, allowing you to focus on your application's business logic instead of maintaining a custom permission engine.

Why Use CASL?

After exploring CASL, I found several advantages over a custom implementation.

  • Built-in support for RBAC and ABAC
  • Field-level permissions
  • Advanced conditional rules
  • TypeScript support
  • Well-tested and widely adopted by the community

Because these features are already implemented, you don't have to reinvent the wheel.

A Simple Example

Creating permissions with CASL is straightforward.

import { AbilityBuilder, createMongoAbility } from "@casl/ability";

const { can, build } = new AbilityBuilder(createMongoAbility);

can("read", "Document");
can("update", "Document", { creatorId: user.id });

const ability = build();

Checking permissions is equally simple.

if (ability.can("update", subject("Document", document))) {
  // User is allowed to update the document
}

Things to Consider

Although CASL is a powerful library, it isn't always the right choice.

Some trade-offs include:

  • A slightly steeper learning curve.
  • Extra configuration compared to a simple RBAC implementation.
  • Additional concepts such as subject() when working with plain JavaScript objects.

For small projects, these trade-offs may not be worth it.

When Should You Use CASL?

Choose CASL WhenAvoid CASL When
Your application has complex authorization rules.Your project only has a few roles and simple permissions.
You need field-level or attribute-based permissions.A basic RBAC implementation is sufficient.
You don't want to maintain a custom permission engine.You prefer keeping dependencies to a minimum.

My Takeaway

One of the biggest lessons I learned is that you don't always need to build your own authorization system.

If your application's permission requirements are relatively simple, a custom RBAC or lightweight ABAC implementation can work well.

However, for large applications with complex authorization rules, using a battle-tested library like CASL can save time, reduce maintenance, and improve reliability.

The important thing isn't choosing the most advanced solution—it's choosing the one that best fits your application's requirements.

Resources

If you'd like to explore authorization and permission systems in more depth, I highly recommend the following resource:

Back to Blog