Hybrid Authorization in ASP.NET Core Identity

Hybrid Authorization in ASP.NET Core Identity: Combining Roles and Claims

In this article, I will discuss  Hybrid Authorization, i.e., combining Role-Based and Claim-Based Authorization in ASP.NET Core Identity. Please read our previous article, where we discussed Policy-Based Authorization in ASP.NET Core Identity.

Modern applications rarely fit neatly into a single authorization style. Roles are ideal for broad classifications, such as admins who can access the admin area. At the same time, claims shine when you need more control by assigning specific permissions, such as CanAddUser, CanDeleteOrder, or CanApprovePayment, directly to users or roles.

In real-world enterprise applications, neither roles nor claims alone can cover all security scenarios effectively. That is where the Hybrid Approach comes in, using the simplicity of roles alongside the flexibility of claims. This ensures both maintainability and precision in enforcing authorization rules.

hybrid approach uses both roles to describe who the user is, claims to describe what they can do, and enforces everything through policies so rules are centralized, auditable, and consistent across controllers, Razor views, and minimal APIs.

What is Role-Based Authorization?

Role-Based Authorization is a mechanism where access decisions are made based on the roles assigned to a user. A role represents a group of permissions bundled under a common name, such as Admin, Manager, Vendor, Customer, etc. When a user belongs to a particular role, they inherit all the permissions associated with it.

For example, an Admin role may automatically grant access to creating users, editing records, and managing system configurations. Role-based checks are straightforward and are typically enforced using the [Authorize(Roles = “Admin”)] attribute in controllers or views. The main advantage of this approach is simplicity, but it often becomes too broad when different users within the same role require slightly different permissions.

What is Claim-Based Authorization?

Claim-Based Authorization is a more flexible mechanism where decisions are made based on claims, which are key-value pairs that represent facts about the user. These claims could be information like EmailConfirmed = true, Department = HR, or permissions like Permission = DeleteUser.

Claims can be assigned to both users and roles, and they are loaded into the ClaimsPrincipal object once the user is authenticated. With claims, developers can enforce complex authorization rules, such as allowing a user to edit only if they have the claim (Permission, EditUser). This allows for very detailed control but can be harder to manage if large groups of users need identical permissions, since you may end up assigning many claims individually.

Why Do We Need the Hybrid Approach in Modern Applications?

Modern enterprise applications are rarely simple. They often require both broad access definitions (roles) and specific permissions (claims). For example:

  • A user in the Manager role should be able to view reports, but only some managers should be allowed to approve financial transactions.
  • An Admin role should have full access, but in some environments, even admins may need claims like CanDeleteUser to perform certain sensitive tasks.

Relying solely on roles can lead to role explosion, too many roles defined to cover every possible variation. On the other hand, relying solely on claims can make administration complex, as managing hundreds of claims for each user becomes difficult.

The Hybrid Approach solves this by:

  • Using roles for broad categorization and grouping (e.g., Admin, Manager, Employee).
  • Using claims for more control (e.g., CanAddUser, CanApproveLoan, CanEditProfile).

This combination provides both ease of management and flexibility, making it the most practical approach for modern applications.

How to Implement a Hybrid Approach in ASP.NET Core Identity?

In ASP.NET Core, policies can combine both role checks and claim checks to implement hybrid authorization. A policy is essentially a rule that defines requirements for accessing a resource.

Syntax in Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ManageUsersPolicy", policy =>
        policy.RequireRole("Admin", "Manager") // Role requirement
              .RequireClaim("Permission", "AddUser", "EditUser")); // Claim requirement
});
Explanation
  • RequireRole(“Admin”, “Manager”) Only users belonging to either Admin or Manager roles can satisfy this part of the policy.
  • RequireClaim(“Permission”, “AddUser”, “EditUser”) In addition to being in the correct role, the user must also hold at least one of the required claims.
  • The result → A user must belong to the specified role and possess the necessary claim to pass authorization.

This hybrid policy ensures that both hierarchical role membership and specific permission checks are enforced simultaneously.

Modifying Program.cs:

Now, in the Program class file, we need to configure authorization policies that will combine both roles (broad access control) and claims (specific permissions). Each policy defines the rules for a particular action in the system. We are going to define the following policies:

  • ViewUsersPolicy → Grants access to Admins, Managers, or anyone who has the ViewUsers claim. This mixes role-based access for higher-level users and claim-based access for exceptions.
  • AddUserPolicy / EditUserPolicy → Allows Admins, or users with AddUser / EditUser claims, to create or modify users without requiring full admin privileges.
  • DeleteUserPolicy → More restrictive; requires both the Admin role and the DeleteUser claim, ensuring only highly trusted users can delete records.
  • ManageUsersPolicy → Provides flexibility; Admins can manage users, but specific claims (EditUser or ManageUsers) also qualify.
  • ManageRolesPolicy → Very sensitive, so it enforces Admin role plus a full set of claims (AddRole, EditRole, DeleteRole). This prevents accidental role mismanagement and enforces least privilege.
  • ManageUserClaimsPolicy → Admins or users with the ManageUserClaims claim can manage claims, giving flexibility for delegated administration.

This structure is called hybrid because each policy checks roles for broad, high-level access while also supporting claims for fine-tuned control. It centralizes all authorization rules in one place, making them clear, auditable, and easy to maintain.

By using policy.RequireAssertion, we can express flexible conditions, for example, granting access if the user belongs to a certain role or has a particular claim. Please replace the AddAuthorization block with the following code.

// Defining Hybrid Policies (Roles + Claims)
builder.Services.AddAuthorization(options =>
{
    // ===== Read users =====
    // Allow: Admins or Managers (broad access) OR anyone with explicit ViewUsers permission
    options.AddPolicy("ViewUsersPolicy", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") ||
            ctx.User.IsInRole("Manager") ||
            ctx.User.HasClaim("Permission", "ViewUsers")));

    // ===== Create users =====
    // Allow: Admins OR explicit AddUser permission
    options.AddPolicy("AddUserPolicy", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") ||
            ctx.User.HasClaim("Permission", "AddUser")));

    // ===== Edit users =====
    // Allow: Admins OR explicit EditUser permission
    options.AddPolicy("EditUserPolicy", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") ||
            ctx.User.HasClaim("Permission", "EditUser")));

    // ===== Delete users =====
    // Allow: Admins AND explicit DeleteUser permission
    options.AddPolicy("DeleteUserPolicy", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") &&
            ctx.User.HasClaim("Permission", "DeleteUser")));

    // ===== Manage users =====
    // Allow: Admins OR (EditUser OR ManageUsers) permission
    options.AddPolicy("ManageUsersPolicy", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") ||
            ctx.User.HasClaim("Permission", "EditUser") ||
            ctx.User.HasClaim("Permission", "ManageUsers")));

    // ===== Manage roles on users (sensitive) =====
    // Require: Admin role AND the full role-management permission set
    // (Change to suit your model; this keeps least-privilege explicit.)
    options.AddPolicy("ManageRolesPolicy", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") &&
            ctx.User.HasClaim("Permission", "AddRole") &&
            ctx.User.HasClaim("Permission", "EditRole") &&
            ctx.User.HasClaim("Permission", "DeleteRole")));

    // ===== Manage user-claims =====
    // Allow: Admins OR explicit ManageUserClaims permission
    options.AddPolicy("ManageUserClaimsPolicy", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") ||
            ctx.User.HasClaim("Permission", "ManageUserClaims")));
});

Note: Please make sure the claim values you reference here (e.g., ViewUsers, AddUser, EditUser, DeleteUser, ManageUsers, AddRole, EditRole, DeleteRole, ManageUserClaims) exist in your claim catalog and are assigned to users/roles as needed.

Applying Policy on UsersController

The following controller uses a hybrid authorization model that combines roles (broad access, who you are) with claims (specific permissions, what you can do). Each [Authorize(Policy = “…”)] attribute delegates the decision to a central policy defined in Program.cs, allowing us to change rules in one place without touching controller logic.

Read vs Change vs Destructive Actions
  • Read (Index, Details → ViewUsersPolicy): Viewing is safer, so access can come from a broad role (Admin/Manager) or a precise ViewUsers claim.
  • Change (Create/Edit → AddUserPolicy, EditUserPolicy): Allow Admin or the exact claim (AddUser, EditUser) so power users can work without full admin rights.
  • Destructive (Delete → DeleteUserPolicy): Requires Admin and the DeleteUser claim. This “AND” rule adds an extra safety net for high-risk operations.
Administration surfaces
  • ManageUsers (ManageUsersPolicy): Access via Admin or a strong capability claim (ManageUsers or EditUser) to keep senior non-admins productive.
  • ManageRoles (ManageRolesPolicy): Very sensitive, so it requires Admin and the full role-management claim set (AddRole, EditRole, DeleteRole). This enforces least privilege and separation of duties.
  • ManageClaims (ManageUserClaimsPolicy): Allow Admin or an explicit ManageUserClaims permission to support controlled delegation.

So, please modify the Users Controller as follows:

using ASPNETCoreIdentityDemo.Services;
using ASPNETCoreIdentityDemo.ViewModels;
using ASPNETCoreIdentityDemo.ViewModels.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace ASPNETCoreIdentityDemo.Controllers
{
// Require authentication for ALL actions in this controller
[Authorize]
public class UsersController : Controller
{
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
public UsersController(IUserService userService, ILogger<UsersController> logger)
{
_userService = userService;
_logger = logger;
}
// GET: /Users
// Hybrid policy: Admin or Manager role OR Permission=ViewUsers
[HttpGet]
[Authorize(Policy = "ViewUsersPolicy")]
public async Task<IActionResult> Index([FromQuery] UserListFilterViewModel filter)
{
try
{
var result = await _userService.GetUsersAsync(filter);
ViewBag.Filter = filter;
return View(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading users list.");
SetError("We couldn’t load the users right now. Please try again.");
return View(new PagedResult<UserListItemViewModel>());
}
}
// GET: /Users/Create
// Hybrid policy: Admin role OR Permission=AddUser
[HttpGet]
[Authorize(Policy = "AddUserPolicy")]
public IActionResult Create()
{
return View(new UserCreateViewModel());
}
// POST: /Users/Create
// Hybrid policy: Admin role OR Permission=AddUser
[HttpPost]
[Authorize(Policy = "AddUserPolicy")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(UserCreateViewModel model)
{
try
{
if (!ModelState.IsValid)
return View(model);
var (result, newId) = await _userService.CreateAsync(model);
if (result.Succeeded)
{
SetSuccess($"User '{model.Email}' was created successfully.");
return RedirectToAction(nameof(Index));
}
AddIdentityErrors(result);
return View(model);
}
catch (DbUpdateException dbx)
{
_logger.LogError(dbx, $"DB error while creating user {model.Email}");
SetError("We couldn’t create the user due to a database error. Please try again.");
return View(model);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error creating user {model.Email}");
SetError("An unexpected error occurred while creating the user.");
return View(model);
}
}
// GET: /Users/Edit/{id}
// Hybrid policy: Admin role OR Permission=EditUser
[HttpGet]
[Authorize(Policy = "EditUserPolicy")]
public async Task<IActionResult> Edit(Guid id)
{
try
{
var userEditViewModel = await _userService.GetForEditAsync(id);
if (userEditViewModel == null)
{
SetError("The user you’re trying to edit was not found.");
return RedirectToAction(nameof(Index));
}
return View(userEditViewModel);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error loading edit form for user {id}");
SetError("We couldn’t load the edit form. Please try again.");
return RedirectToAction(nameof(Index));
}
}
// POST: /Users/Edit
// Hybrid policy: Admin role OR Permission=EditUser
[HttpPost]
[Authorize(Policy = "EditUserPolicy")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(UserEditViewModel model)
{
try
{
if (!ModelState.IsValid)
return View(model);
var result = await _userService.UpdateAsync(model);
if (result.Succeeded)
{
SetSuccess("User was updated successfully.");
return RedirectToAction(nameof(Index));
}
if (result.Errors.Any(e =>
string.Equals(e.Code, "ConcurrencyFailure", StringComparison.OrdinalIgnoreCase)))
{
SetError("This user was modified by another admin. Please reload the page and try again.");
}
AddIdentityErrors(result);
return View(model);
}
catch (DbUpdateConcurrencyException cex)
{
_logger.LogWarning(cex, $"Concurrency error updating user {model.Id}");
SetError("Your changes could not be saved because another update occurred. Please reload and try again.");
return View(model);
}
catch (DbUpdateException dbx)
{
_logger.LogError(dbx, $"DB error while updating user {model.Id}");
SetError("We couldn’t update the user due to a database error. Please try again.");
return View(model);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error updating user {model.Id}");
SetError("An unexpected error occurred while updating the user.");
return View(model);
}
}
// GET: /Users/Details/{id}
// Hybrid policy: Admin or Manager role OR Permission=ViewUsers
[HttpGet]
[Authorize(Policy = "ViewUsersPolicy")]
public async Task<IActionResult> Details(Guid id)
{
try
{
var userDetailsViewModel = await _userService.GetDetailsAsync(id);
if (userDetailsViewModel == null)
{
SetError("The requested user was not found.");
return RedirectToAction(nameof(Index));
}
return View(userDetailsViewModel);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error loading details for user {id}");
SetError("We couldn’t load the user details. Please try again.");
return RedirectToAction(nameof(Index));
}
}
// GET: /Users/Delete/{id}
// Hybrid policy: Admin role OR Permission=DeleteUser
[HttpGet]
[Authorize(Policy = "DeleteUserPolicy")]
public async Task<IActionResult> Delete(Guid id)
{
try
{
var userDetailsViewModel = await _userService.GetDetailsAsync(id);
if (userDetailsViewModel == null)
{
SetError("The user you’re trying to delete was not found.");
return RedirectToAction(nameof(Index));
}
return View(userDetailsViewModel);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error loading delete confirmation for user {id}");
SetError("We couldn’t load the delete confirmation. Please try again.");
return RedirectToAction(nameof(Index));
}
}
// POST: /Users/DeleteConfirmed
// Hybrid policy: Admin role and must have Permission=DeleteUser
[HttpPost]
[Authorize(Policy = "DeleteUserPolicy")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid id)
{
if (id == Guid.Empty) return NotFound();
try
{
var result = await _userService.DeleteAsync(id);
if (result.Succeeded)
{
SetSuccess("User was deleted successfully.");
return RedirectToAction(nameof(Index));
}
if (result.Errors.Any(e => string.Equals(e.Code, "LastAdmin", StringComparison.OrdinalIgnoreCase)))
{
SetError("You cannot delete the last user in the ‘Admin’ role.");
}
else if (result.Errors.Any(e => string.Equals(e.Code, "NotFound", StringComparison.OrdinalIgnoreCase)))
{
SetError("The user no longer exists.");
}
else
{
SetError(string.Join(" ", result.Errors.Select(e => e.Description)));
}
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException dbx)
{
_logger.LogError(dbx, $"DB error while deleting user {id}");
SetError("We couldn’t delete the user due to a database error. Please try again.");
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error deleting user {id}");
SetError("An unexpected error occurred while deleting the user.");
return RedirectToAction(nameof(Index));
}
}
// GET: /Users/ManageRoles/{id}
// Hybrid policy: Admin role AND Permission set (AddRole, EditRole, DeleteRole)
[HttpGet]
[Authorize(Policy = "ManageRolesPolicy")]
public async Task<IActionResult> ManageRoles(Guid id)
{
try
{
var userRolesEditViewModel = await _userService.GetRolesForEditAsync(id);
if (userRolesEditViewModel == null)
{
SetError("The user was not found.");
return RedirectToAction(nameof(Index));
}
return View(userRolesEditViewModel);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error loading roles editor for user {id}");
SetError("We couldn’t load the roles editor. Please try again.");
return RedirectToAction(nameof(Index));
}
}
// POST: /Users/ManageRoles
// Hybrid policy: Admin role AND Permission set (AddRole, EditRole, DeleteRole)
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Policy = "ManageRolesPolicy")]
public async Task<IActionResult> ManageRoles(UserRolesEditViewModel model)
{
if (!ModelState.IsValid)
return View(model);
try
{
var selected = model.Roles.Where(r => r.IsSelected).Select(r => r.RoleId).ToList();
var result = await _userService.UpdateRolesAsync(model.UserId, selected);
if (result.Succeeded)
{
SetSuccess("User roles were updated successfully.");
return RedirectToAction(nameof(Details), new { id = model.UserId });
}
if (result.Errors.Any(e => string.Equals(e.Code, "RoleNotFound", StringComparison.OrdinalIgnoreCase)))
{
SetError("One or more selected roles no longer exist. Please refresh and try again.");
}
AddIdentityErrors(result);
var userRolesEditViewModel = await _userService.GetRolesForEditAsync(model.UserId);
return View(userRolesEditViewModel ?? model);
}
catch (DbUpdateException dbx)
{
_logger.LogError(dbx, $"DB error while updating roles for user {model.UserId}");
SetError("We couldn’t update roles due to a database error. Please try again.");
var vm = await _userService.GetRolesForEditAsync(model.UserId);
return View(vm ?? model);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error updating roles for user {model.UserId}");
SetError("An unexpected error occurred while updating roles.");
var vm = await _userService.GetRolesForEditAsync(model.UserId);
return View(vm ?? model);
}
}
// GET: /Users/ManageClaims/{id}
// Hybrid policy: Admin role OR Permission=ManageUserClaims
[HttpGet]
[Authorize(Policy = "ManageUserClaimsPolicy")]
public async Task<IActionResult> ManageClaims(Guid id)
{
var userClaimsEditViewModel = await _userService.GetClaimsForEditAsync(id);
if (userClaimsEditViewModel == null)
{
SetError("The user was not found.");
return RedirectToAction(nameof(Index));
}
return View(userClaimsEditViewModel);
}
// POST: /Users/ManageClaims
// Hybrid policy: Admin role OR Permission=ManageUserClaims
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Policy = "ManageUserClaimsPolicy")]
public async Task<IActionResult> ManageClaims(UserClaimsEditViewModel model)
{
if (!ModelState.IsValid)
return View(model);
try
{
var selected = model.Claims.Where(c => c.IsSelected).Select(c => c.ClaimId).ToList();
var result = await _userService.UpdateClaimsAsync(model.UserId, selected);
if (result.Succeeded)
{
SetSuccess("User claims were updated successfully.");
return RedirectToAction(nameof(Details), new { id = model.UserId });
}
if (result.Errors.Any(e => string.Equals(e.Code, "InvalidClaimSelection", StringComparison.OrdinalIgnoreCase)))
SetError("One or more selected claims are not assignable to users. Please refresh and try again.");
AddIdentityErrors(result);
var reload = await _userService.GetClaimsForEditAsync(model.UserId);
return View(reload ?? model);
}
catch (DbUpdateException dbx)
{
_logger.LogError(dbx, "DB error while updating claims for user {UserId}", model.UserId);
SetError("We couldn’t update claims due to a database error. Please try again.");
var reload = await _userService.GetClaimsForEditAsync(model.UserId);
return View(reload ?? model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error updating claims for user {UserId}", model.UserId);
SetError("An unexpected error occurred while updating claims.");
var reload = await _userService.GetClaimsForEditAsync(model.UserId);
return View(reload ?? model);
}
}
#region Helpers
// Helper: Push a success message into TempData (survives redirect)
private void SetSuccess(string message)
{
TempData["Success"] = message;
}
// Helper: Push an error message into TempData (survives redirect)
private void SetError(string message)
{
TempData["Error"] = message;
}
// Helper: Copy IdentityResult errors into ModelState 
// so they can be displayed by validation summary in views.
private void AddIdentityErrors(IdentityResult result)
{
if (result == null || result.Succeeded) return;
foreach (var e in result.Errors)
ModelState.AddModelError(string.Empty, e.Description);
}
#endregion
}
}

Note: If you later want to update any rule (e.g., require Admin OR DeleteUser for deletions), you only touch the policy in Program.cs; the controller code stays the same.

Applying Hybrid Policies on Views:

The following User Controller Index view applies policy-based authorization right in the UI layer to control which actions (buttons/links) should be visible to the signed-in user. By checking the policies here that the controller enforces, our UI stays consistent with server-side security.

Why inject IAuthorizationService?

Views don’t use [Authorize] attributes. Injecting IAuthorizationService allows the view to query the central policy engine and determine whether the current user meets each policy (e.g., “AddUserPolicy”). This keeps the view aligned with rules defined in Program.cs.

Evaluate once, reuse everywhere.

The code runs each policy check once at the top (e.g., canEditUser, canManageRoles) and then reuses those booleans while rendering the table. This avoids repeated async calls and keeps the markup clean and readable.

Hybrid logic reflected in the UI

Each Boolean maps to a hybrid rule:

  • canViewDetails → Admin or Manager role or Permission=ViewUsers
  • canCreateUser → Admin role or Permission=AddUser
  • canEditUser → Admin role or Permission=EditUser
  • canDeleteUser → Admin role and Permission=DeleteUser (destructive → stricter)
  • canManageRoles → Admin role and AddRole + EditRole + DeleteRole (most sensitive)
  • canManageClaims → Admin role or Permission=ManageUserClaims (delegated admin)
UI as guidance, controller as gate

Hiding buttons improves UX and reduces accidental clicks, but it’s not a security boundary. The controller actions still enforce the same policies via [Authorize(Policy = “…”)]. If someone calls an endpoint directly, the server will still block them. This is defense-in-depth.

So, please modify the Index View of the Users Controller as follows:

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
@using ASPNETCoreIdentityDemo.ViewModels
@using ASPNETCoreIdentityDemo.ViewModels.Users
@model PagedResult<UserListItemViewModel>
@{
ViewData["Title"] = "Users";
var filter = (UserListFilterViewModel)ViewBag.Filter;
// HYBRID POLICY CHECKS (roles OR claims depending on your Program.cs rules)
// These run once per page render, then we reuse the booleans in the table.
var canCreateUser = (await AuthorizationService.AuthorizeAsync(User, "AddUserPolicy")).Succeeded;          // Admin OR Permission=AddUser
var canEditUser = (await AuthorizationService.AuthorizeAsync(User, "EditUserPolicy")).Succeeded;         // Admin OR Permission=EditUser
var canDeleteUser = (await AuthorizationService.AuthorizeAsync(User, "DeleteUserPolicy")).Succeeded;       // Admin OR Permission=DeleteUser
var canViewDetails = (await AuthorizationService.AuthorizeAsync(User, "ViewUsersPolicy")).Succeeded;        // Admin OR Manager OR Permission=ViewUsers
var canManageRoles = (await AuthorizationService.AuthorizeAsync(User, "ManageRolesPolicy")).Succeeded;      // Admin AND (AddRole & EditRole & DeleteRole)
var canManageClaims = (await AuthorizationService.AuthorizeAsync(User, "ManageUserClaimsPolicy")).Succeeded;  // Admin OR Permission=ManageUserClaims
// Show Actions column if any action is permitted
bool showActionsCol = canViewDetails || canEditUser || canManageRoles || canDeleteUser || canManageClaims;
}
<div class="container mt-1">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4 pb-2 border-bottom">
<div>
<h1 class="h3 mb-1 fw-bold text-primary">Users Administration</h1>
<p class="text-muted mb-0">Search, filter, and manage application users.</p>
</div>
@* Create User: allowed by hybrid policy (Admin OR Permission=AddUser) *@
@if (canCreateUser)
{
<div>
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i> Add New User
</a>
</div>
}
</div>
@if (TempData["Success"] is string sMsg)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>@sMsg
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (TempData["Error"] is string eMsg)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>@eMsg
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
<!-- Filter Bar -->
<form method="get" class="row g-2 align-items-end mb-3">
<div class="col-md-4">
<label class="form-label">Search</label>
<input name="Search" value="@filter.Search" class="form-control"
placeholder="Search name, email, phone, username" />
</div>
<div class="col-md-2">
<label class="form-label">Status</label>
<select name="IsActive" class="form-select">
<option value="">All Status</option>
<option value="true" selected="@(filter.IsActive == true)">Active</option>
<option value="false" selected="@(filter.IsActive == false)">Inactive</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Email</label>
<select name="EmailConfirmed" class="form-select">
<option value="">All Emails</option>
<option value="true" selected="@(filter.EmailConfirmed == true)">Confirmed</option>
<option value="false" selected="@(filter.EmailConfirmed == false)">Unconfirmed</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Page Size</label>
<select name="PageSize" class="form-select">
@foreach (var size in new[] { 5, 10, 20, 50 })
{
<option value="@size" selected="@(filter.PageSize == size)">@size / page</option>
}
</select>
</div>
<div class="col-md-2 d-grid d-sm-flex gap-2">
<button class="btn btn-primary" type="submit">
<i class="bi bi-funnel me-1"></i> Apply
</button>
<!-- Clear = go to Index without query string -->
<a asp-action="Index" class="btn btn-info">
<i class="bi bi-x-circle me-1"></i> Clear
</a>
</div>
</form>
<div class="card shadow border-0 rounded">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Status</th>
<th>Email Status</th>
<th>Created</th>
@if (showActionsCol)
{
<th class="text-end">Actions</th>
}
</tr>
</thead>
<tbody>
@if (!Model.Items.Any())
{
<tr>
<td colspan="@(showActionsCol ? 7 : 6)" class="text-center text-muted py-4">No users found.</td>
</tr>
}
else
{
foreach (var u in Model.Items)
{
<tr>
<td>@($"{u.FirstName} {u.LastName}".Trim())</td>
<td>@u.Email</td>
<td>@u.PhoneNumber</td>
<td>
@if (u.IsActive)
{
<span class="badge rounded-pill bg-success">Active</span>
}
else
{
<span class="badge rounded-pill bg-danger">Inactive</span>
}
</td>
<td>
@if (u.EmailConfirmed)
{
<span class="badge rounded-pill bg-success">Confirmed</span>
}
else
{
<span class="badge rounded-pill bg-warning text-dark">Unconfirmed</span>
}
</td>
<td>@u.CreatedOn?.ToString("yyyy-MM-dd")</td>
@if (showActionsCol)
{
<td class="text-end">
@* DETAILS (hybrid): Admin OR Manager OR Permission=ViewUsers *@
@if (canViewDetails)
{
<a asp-action="Details" asp-route-id="@u.Id" class="btn btn-sm btn-outline-info me-1">
<i class="bi bi-card-list me-1"></i> Details
</a>
}
@* EDIT (hybrid): Admin OR Permission=EditUser *@
@if (canEditUser)
{
<a asp-action="Edit" asp-route-id="@u.Id" class="btn btn-sm btn-outline-primary me-1">
<i class="bi bi-pencil-square me-1"></i> Edit
</a>
}
@* MANAGE CLAIMS (hybrid): Admin OR Permission=ManageUserClaims *@
@if (canManageClaims)
{
<a asp-action="ManageClaims" asp-route-id="@u.Id" class="btn btn-sm btn-outline-success">
<i class="bi bi-key me-1"></i> Claims
</a>
}
@* MANAGE ROLES (hybrid): Admin AND (AddRole & EditRole & DeleteRole) *@
@if (canManageRoles)
{
<a asp-action="ManageRoles" asp-route-id="@u.Id" class="btn btn-sm btn-outline-dark me-1">
<i class="bi bi-shield-check me-1"></i> Roles
</a>
}
@* DELETE (hybrid): Admin AND Permission=DeleteUser *@
@if (canDeleteUser)
{
<a asp-action="Delete" asp-route-id="@u.Id" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash me-1"></i> Delete
</a>
}
</td>
}
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>
<partial name="~/Views/Shared/_Pager.cshtml"
model="new PagedResult<object> { Items = Array.Empty<object>(), TotalCount = Model.TotalCount, PageNumber = Model.PageNumber, PageSize = Model.PageSize }" />
</div>
Differences Between Role-Based Authorization and Claim-Based Authorization

The key difference lies in granularity and flexibility:

  • Role-Based Authorization is high-level; it grants access based on group membership. Roles are simple to manage and are best for defining high-level access, like Admin or User.
  • Claim-Based Authorization is detailed and specific; it allows decisions based on specific attributes or permissions assigned to a user. Claims are more precise but require careful management to avoid complexity.

In short, roles answer “Who are you in the system?”, while claims answer “What can you do in the system?”.

Authorization is not one-size-fits-all. Role-Based Authorization provides simplicity and ease of grouping, while Claim-Based Authorization offers more control and flexibility. However, enterprise applications typically need both broad role assignments and claim checks. This is why the Hybrid Approach, combining roles and claims through policies, becomes the most effective strategy.

By using this approach, organizations can ensure that authorization is both scalable for administrators and granular enough for sensitive operations.

In the next article, I will discuss External Identity Providers in ASP.NET Core Identity. In this article, I explain Role Claims-Based Authorization in ASP.NET Core Identity. I hope you enjoy this article, Role Claims-Based Authorization in ASP.NET Core Identity.

4 thoughts on “Hybrid Authorization in ASP.NET Core Identity”

  1. Thank you for all the effort you made. I benefited a lot and created the ASPNETCoreIdentityDemo project through the explanation.
    Thanks again

  2. blank
    Ibrahim Khalil Shakik

    Thank you for all the effort you made. I benefited a lot and created the ASPNETCoreIdentityDemo project through the explanation.
    Thanks again

Leave a Reply

Your email address will not be published. Required fields are marked *