RBAC vs ABAC — Choosing the Right Authorization Model
Role-Based vs Attribute-Based vs Relationship-Based Access Control with real examples, ASP.NET Core implementation, and guidance for healthcare and multi-tenant apps.
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
// 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).
// 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:
[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_folderReBAC 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:
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.
Enjoyed this article?
Explore the Security & Compliance learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.