Web Security & Ethical Hacking · Lesson 11 of 23

RBAC vs ABAC — Role and Attribute Based Access Control

Authorization vs Authentication

Authentication answers "who are you?" Authorization answers "what are you allowed to do?" They're separate concerns, and most security failures in enterprise apps are authorization failures, not authentication failures.

A user who successfully authenticates can still access data they shouldn't if authorization is wrong. This article covers three models for getting authorization right.

Role-Based Access Control (RBAC)

RBAC assigns users to roles, and roles have permissions. It's simple, well-understood, and works well at small to medium scale.

User → Role(s) → Permissions
Alice → [Doctor, Staff] → [read:patients, write:notes, view:schedule]
Bob → [Receptionist] → [view:schedule, read:patient-names]

Where RBAC shines: applications with a clear, stable set of job functions where access aligns neatly with those functions.

Where RBAC breaks down: as requirements become more specific, roles multiply. "Doctor" becomes "AttendingDoctor", "ConsultingDoctor", "OnCallDoctor". You add "SeniorDoctor", "JuniorDoctor". Soon you have 40 roles and the combinations are unmanageable. This is called role explosion.

The deeper problem: RBAC can't express context. "A doctor can read patient records" is fine. "A doctor can only read records of patients assigned to them" requires something more.

Implementing RBAC in ASP.NET Core

C#
// Assign roles at login
var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, userId),
    new Claim(ClaimTypes.Role, "Doctor"),
    new Claim(ClaimTypes.Role, "Staff")
};

// Protect endpoints by role
[Authorize(Roles = "Doctor,Nurse")]
[HttpGet("patients/{id}/records")]
public async Task<IActionResult> GetMedicalRecord(int id) { ... }

// Or use policies (more flexible than direct role checks)
services.AddAuthorization(options =>
{
    options.AddPolicy("CanAccessMedicalRecords", policy =>
        policy.RequireRole("Doctor", "Nurse", "Pharmacist"));

    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin")
              .RequireClaim("department", "IT"));
});

[Authorize(Policy = "CanAccessMedicalRecords")]
public IActionResult GetRecord() { ... }

Attribute-Based Access Control (ABAC)

ABAC evaluates access based on attributes — of the user, the resource, the environment, and the action. It uses policies expressed as rules over these attributes.

Policy: Allow access IF
  user.role == "Doctor"
  AND resource.patientId IN user.assignedPatients
  AND environment.time BETWEEN 06:00 AND 22:00
  AND action == "read"

This expresses "a doctor can read records of patients currently assigned to them, during working hours." RBAC cannot express this. ABAC can.

ABAC attributes:

  • User attributes: role, department, clearance level, location, specialization
  • Resource attributes: classification, owner, department, sensitivity, patient ID
  • Environment attributes: time, IP address, device trust level
  • Action attributes: read, write, delete, export

Resource-Based Authorization in ASP.NET Core

ASP.NET Core's policy infrastructure supports ABAC via resource-based authorization. The key is IAuthorizationService.AuthorizeAsync(user, resource, requirement).

C#
// Define the requirement
public class PatientAccessRequirement : IAuthorizationRequirement { }

// Define the handler — this is where the ABAC logic lives
public class PatientAccessHandler
    : AuthorizationHandler<PatientAccessRequirement, Patient>
{
    private readonly IPatientAssignmentService _assignments;

    public PatientAccessHandler(IPatientAssignmentService assignments)
    {
        _assignments = assignments;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PatientAccessRequirement requirement,
        Patient patient)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var userRole = context.User.FindFirst(ClaimTypes.Role)?.Value;

        // Admins always pass
        if (userRole == "Admin")
        {
            context.Succeed(requirement);
            return;
        }

        // Doctors only if assigned to this patient
        if (userRole == "Doctor")
        {
            var isAssigned = await _assignments.IsAssignedAsync(userId!, patient.Id);
            if (isAssigned)
                context.Succeed(requirement);
            return;
        }

        // Emergency override: any clinician if patient is in emergency
        if (patient.IsEmergency && context.User.IsInRole("Clinician"))
        {
            context.Succeed(requirement);
        }
    }
}

// Register
services.AddSingleton<IAuthorizationHandler, PatientAccessHandler>();
services.AddAuthorization(options =>
{
    options.AddPolicy("CanAccessPatient",
        policy => policy.Requirements.Add(new PatientAccessRequirement()));
});

Use in a controller:

C#
[Authorize]
[HttpGet("patients/{id}")]
public async Task<IActionResult> GetPatient(int id)
{
    var patient = await _repo.GetByIdAsync(id);
    if (patient == null) return NotFound();

    // Resource-based check — passes the actual resource to the handler
    var authResult = await _authorizationService.AuthorizeAsync(
        User, patient, "CanAccessPatient");

    if (!authResult.Succeeded)
        return Forbid(); // 403, not 404 — don't reveal existence

    return Ok(patient);
}

Relationship-Based Access Control (ReBAC)

ReBAC defines access based on the relationship between a user and a resource. Used by Google (Zanzibar system), Auth0 FGA, and SpiceDB.

tuples (facts stored in the system):
  patient:123#assigned_doctor → user:alice
  patient:123#assigned_nurse → user:bob
  folder:reports#owner → user:carol
  folder:reports#viewer → group:analysts

rules:
  can_view patient IF user is assigned_doctor OR assigned_nurse OR admin
  can_edit document IF user is owner of parent_folder

ReBAC is the right choice when permissions are graph-shaped — teams, organizations, resource hierarchies, sharing models (like Google Drive). It's complex to implement from scratch; use Auth0 FGA or SpiceDB for serious implementations.

Comparison

| Model | Best For | Limitation | |-------|----------|-----------| | RBAC | Clear job functions, stable roles | Role explosion, can't express context | | ABAC | Context-sensitive rules, fine-grained | Complex to implement and audit | | ReBAC | Graph-shaped relationships, sharing models | Infrastructure overhead |

Healthcare and Multi-Tenant Applications

Healthcare apps almost always need ABAC. HIPAA's minimum necessary standard requires access to be limited to what's needed for a specific purpose — which RBAC can't enforce without exploding into hundreds of roles.

Pattern for multi-tenant + healthcare:

C#
public class MultiTenantPatientHandler
    : AuthorizationHandler<PatientAccessRequirement, Patient>
{
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PatientAccessRequirement requirement,
        Patient patient)
    {
        var userTenantId = context.User.FindFirst("tenant_id")?.Value;

        // Tenant isolation — hard stop, never cross tenants
        if (patient.TenantId != userTenantId)
        {
            context.Fail(); // Explicitly fail, not just not-succeed
            return;
        }

        // Within the tenant, apply role + assignment rules
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        // ... rest of the logic
    }
}

Always call context.Fail() (not just skip Succeed()) when crossing tenant boundaries. The difference: Fail() prevents other handlers from succeeding; not-calling-Succeed allows another handler to grant access.

Practical Recommendation

Start with RBAC for coarse-grained access (which features a user can see). Layer ABAC on top for resource-level decisions (which specific records they can access). Don't try to express everything in roles.

The ASP.NET Core policy system supports both in the same application — use role policies for endpoint-level guards and resource-based handlers for row-level authorization.