Role Based Authorization in ASP.NET Core Identity

Role-Based Authorization (RBAC) in ASP.NET Core Identity 

In this article, I will discuss Role-Based Authorization in ASP.NET Core IdentityPlease read our previous article discussing User Management in ASP.NET Core Identity. Role-Based Authorization (RBAC) is one of the most powerful access control mechanisms in modern web applications. It ensures that only the right people can perform the right tasks within your application. We will discuss the following pointers:

  1. What is a Role?
  2. What is Role-Based Access Control (RBAC)?
  3. What is Authentication and Authorization?
  4. Why Do We Need Role-Based Authorization?
  5. What is Access Denied?
  6. How to Implement Role-Based Authorization in ASP.NET Core?
  7. What Happens When a User Tries to Access a Page They’re Not Authorized To?
  8. How to Configure Access Denied Page in ASP.NET Core MVC?
  9. Why Role-Based Authorization in Views?
  10. How to Show or Hide the Navigation Menu Based on User Role?
What is a Role?

A role is a named group (e.g., Admin, Manager, Customer) that represents a set of permissions. Instead of assigning permissions to each user individually (which is messy and error-prone), we assign users to roles and authorize features based on role membership. Think of a school.

  • Teachers can mark attendance and grade papers.
  • Students can submit homework.
  • Principals can view all classes.

Each role carries certain permissions or responsibilities. Instead of assigning permissions directly to each user, we assign a role, and the role contains the permissions. In ASP.NET Core Identity:

  • Roles are stored in the AspNetRoles table (in our case, it is the Roles table).
  • A user can have one or more roles.
  • Role names are like “Admin”, “Customer”, “Manager”, etc.
What is Authentication and Authorization?

These two concepts are related but different:

Authentication – Who are you?

This is the process of verifying identity. In ASP.NET Core Identity, authentication happens when you log in with valid credentials. For example: Logging in with email and password → If correct, you are authenticated.

Authorization – What can you do?

This determines what you are allowed to do after authentication. For example, after logging in, the system checks your role to see if you can access the Admin page.

What is Role-Based Access Control (RBAC)?

RBAC means controlling who can do what in the system based on their assigned role.
Instead of assigning individual permissions to each user, we assign them a role, and that role already contains the required permissions.

How RBAC Works:

  1. Define Roles → Admin, Manager, User.
  2. Assign Roles to Users → John = Admin, Sara = User.
  3. Check Roles Before Access → If John tries to open the Admin Dashboard → Allowed.
    If Sara tries → Access Denied.

A library has Members and Librarians. Members can borrow books; Librarians can add/remove books. The software checks your role each time you try to perform an action.

Why Do We Need Role-Based Authorization?

Without role-based checks, every user could access everything, which is dangerous and not recommended. Imagine managing 100 employees. Would you manually assign “edit, delete, create, view, etc.” to each one? No! Instead, you create roles like “Editor”, “Viewer”, and assign them to users.

Reasons we need it:

  1. Security – Prevent unauthorized access to sensitive features.
  2. Consistency – Everyone in the same role gets the same permissions.
  3. Scalability – Assigning roles is faster than setting individual permissions for each user.
  4. Compliance – Many industries (banking, healthcare) require strict access control.

Example: In a shop, only Cashiers can open the cash drawer; giving that power to every employee would be risky.

What is Access Denied?

Access Denied means the user is authenticated (logged in) but not authorized to perform the action or see the page. In HTTP terms, this is 403 Forbidden.

  • Unauthenticated401 Unauthorized → the app redirects to LoginPath.
  • Authenticated but forbidden403 Forbidden → the app redirects to AccessDeniedPath.

Example: For example, a logged-in user tries to access /Admin/Dashboard but does not have the “Admin” role (not authorized) → Access Denied.

How to Implement Role-Based Authorization in ASP.NET Core?

We will implement the following five scenarios to understand Role-Based Authorization. Please remember we need to use Authorize Attribute along with the Roles property to implement Role-Based Authorization in ASP.NET Core MVC.

Public Endpoint – No Authentication Required

Anyone can access this endpoint, even without logging in. Used for public pages like Home, About, Contact, Product listings, FAQs, etc. No authentication check is done.

Syntax: (No [Authorize] attribute), The [AllowAnonymous] overrides any global or controller-level [Authorize] and allows unauthenticated users.

[AllowAnonymous] // Optional, only needed if controller has [Authorize] globally
public IActionResult PublicPage()
{
    return View();
}
Any Authenticated User (No Role Required)

Only logged-in users can access this endpoint. Doesn’t matter what role they have, as long as they are authenticated. Useful for profile pages, dashboards, and user settings.

Syntax: Only requires a valid login session. No role check is performed.

[Authorize] // requires authentication but no specific role
public IActionResult UserProfile()
{
    return View();
}
Requires Single Role: Admin

Only logged-in users with the “Admin” role can access this endpoint. Useful for Admin Panel, User Management, and System Configurations.

Syntax: If the user is not in the “Admin” role, they get redirected to the Access Denied page.

[Authorize(Roles = "Admin")]
public IActionResult AdminDashboard()
{
    return View();
}
Requires Multiple Roles (OR): Admin OR Manager

User must be logged in and have either the “Admin” or “Manager” role. The roles are OR-ed together. For example, approving leave requests can be done by an Admin or a Manager.

Syntax: This is an OR condition by default. If the user is in either role, access is granted.

[Authorize(Roles = "Admin,Manager")] // Comma means OR condition
public IActionResult ApproveRequests()
{
    return View();
}
Requires multiple roles (AND): Both Admin and Manager

User must be logged in and have both roles at the same time. Less common and is used in cases where a person must have two responsibilities. To get AND, add two requirements, so both must pass.

Syntax: This is an AND condition. If the user is in both roles, access is granted.

[Authorize(Roles = "Admin")]
[Authorize(Roles = "Manager")] 
public IActionResult SpecialReport()
{
    return View();
}
Example to Understand Role-Based Access Control in ASP.NET Core MVC:

Now, we will modify the Users Controller to implement Role-Based Access Control as follows:

  • Index, Details: Any authenticated user (no role needed)
  • Create (GET/POST), Delete (GET/POST): Single role: Admin
  • Edit (GET/POST): Multiple roles (OR): Admin or Manager
  • ManageRoles (GET/POST): Multiple roles (AND): Admin and Manager

We already have [Authorize] at the controller level, so everything requires authentication by default. For “any authenticated user”, we don’t need any extra attribute on the action. We are adding explicit attributes where role checks are required. So, please modify the UsersController 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;
using System.Security.Claims;
namespace ASPNETCoreIdentityDemo.Controllers
{
// Any authenticated user (no role required) for the whole controller by default
[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;
}
// Any authenticated user (no specific role required)
// [Authorize] // (redundant; covered by class-level)
[HttpGet]
public async Task<IActionResult> Index([FromQuery] UserListFilterViewModel filter)
{
// List page is read-only; exceptions here are unlikely, but log & show friendly error.
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>()); 
}
}
// Requires single role: Admin
[Authorize(Roles = "Admin")]
[HttpGet]
public IActionResult Create()
{
return View(new UserCreateViewModel());
}
// Requires single role: Admin
[Authorize(Roles = "Admin")]
[HttpPost]
[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)
{
// Most common: unique index conflicts or other DB issues
_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);
}
}
// Requires multiple roles (OR): Admin or Manager
[Authorize(Roles = "Admin,Manager")] // OR semantics
[HttpGet]
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));
}
}
// Requires multiple roles (OR): Admin or Manager
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin,Manager")] // OR semantics
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));
}
// Detect optimistic concurrency from service error code and show friendlier message
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);
}
}
// Any authenticated user (no specific role required)
// [Authorize] // (redundant; covered by class-level)
[HttpGet]
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));
}
}
// Requires single role: Admin
[Authorize(Roles = "Admin")]
[HttpGet]
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); // confirm page
}
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));
}
}
// Requires single role: Admin
[Authorize(Roles = "Admin")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid id)
{
if (id == Guid.Empty) return NotFound();
try
{
var currentUserId = Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var g) ? g : Guid.Empty;
var result = await _userService.DeleteAsync(id);
if (result.Succeeded)
{
SetSuccess("User was deleted successfully.");
return RedirectToAction(nameof(Index));
}
// Last Admin cannot be deleted
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));
}
}
// Requires multiple roles (AND): Both Admin and Manager 
[HttpGet]
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Manager")] // AND overall
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));
}
}
// Requires multiple roles (AND): Both Admin and Manager
[HttpPost, ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Manager")] // AND overall
public async Task<IActionResult> ManageRoles(UserRolesEditViewModel model)
{
if (model == null || model.UserId == Guid.Empty) 
return NotFound();
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 });
}
// Surface specific role errors cleanly
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);
// Reload editor if failed (to re-populate checkbox list accurately)
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);
}
}
#region Helpers
// Push a success message to TempData.
private void SetSuccess(string message)
{
TempData["Success"] = message;
}
// Push an error message to TempData.
private void SetError(string message)
{
TempData["Error"] = message;
}
// Adds IdentityResult errors into ModelState for field and model-level display.
private void AddIdentityErrors(IdentityResult result)
{
if (result == null || result.Succeeded) 
return;
foreach (var e in result.Errors)
{
ModelState.AddModelError(string.Empty, e.Description);
}
}
#endregion
}
}

With the above changes in place, run the application and test the functionality.

What Happens When a User Tries to Access a Page They’re Not Authorized To?

In ASP.NET Core, two different things can happen depending on whether the user is logged in or not:

User is NOT Authenticated
  • Meaning: They are not logged in.
  • Result: ASP.NET Core responds with HTTP 401 (Unauthorized) internally, and for MVC applications, it redirects to the login page.
  • Why: The system first needs to confirm “Who are you?” before checking permissions.

Example: A guest tries to visit /Users/Create (which requires login). They are redirected to /Account/Login?ReturnUrl=%2FUsers%2FCreate.

User IS Authenticated but NOT Authorized
  • Meaning: They are logged in but don’t have the required role or meet the policy.
  • Result: ASP.NET Core responds with HTTP 403 (Forbidden).
    In MVC/Razor Pages, if AccessDeniedPath is configured, the user will be redirected to that page.
  • Why: The system knows who the user is, but they lack permission to perform the action.

Example: A user with the role “Manager” tries to visit /Users/Create, which is restricted to “Admin” only. They are redirected to /Account/AccessDenied with a message like You do not have permission to view this page as shown in the image below.

Role Based Authorization in ASP.NET Core Identity

How to Configure Access Denied Page in ASP.NET Core MVC?

By default, the path for unauthenticated users is /Account/AccessDenied, but you can also change this default access denied path. We configure the login path, in the same way we can also need to configure the Access Denied path within the Program.cs class file, as follows. Here, we need to use the AccessDeniedPath property of the CookieAuthenticationOptions object.

// Configure the Application Cookie settings
builder.Services.ConfigureApplicationCookie(options =>
{
// If the LoginPath isn't set, ASP.NET Core defaults the path to /Account/Login.
options.LoginPath = "/Account/Login"; // Set your login path here
// If the AccessDenied isn't set, ASP.NET Core defaults the path to /Account/AccessDenied
options.AccessDeniedPath = "/Account/AccessDenied"; // Set your access denied path here
});
Create the AccessDenied Endpoint in Account Controller

Please add the following HttpGet AccessDenied action method within the AccountController. We want this action method to be accessed by everyone, so we are decorating it with the AllowAnonymous Attribute.

[AllowAnonymous]
[HttpGet]
public IActionResult AccessDenied(string? returnUrl = null)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
AccessDenied View

Next, create a view named AccessDenied.cshtml within the Views/Account directory and then copy and paste the following code:

@using System.Security.Claims
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment Env
@{
ViewData["Title"] = "Access Denied";
var returnUrl = ViewBag.ReturnUrl as string;
var isLocal = !string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl);
}
<div class="container py-1">
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-7">
<div class="card shadow border-danger">
<div class="card-header text-bg-danger">
<h1 class="h4 mb-0">Access Denied</h1>
</div>
<div class="card-body">
<p class="mb-3">
You’re signed in but don’t have permission to view this page.
</p>
<div class="alert alert-danger" role="alert">
<div class="d-flex">
<div class="me-2" aria-hidden="true"><strong>403 - Forbidden:</strong> </div>
<div>
If you believe this is a mistake, please contact an administrator or request access.
</div>
</div>
</div>
@if (isLocal)
{
<p class="small text-muted mb-4">
Requested URL: <code>@returnUrl</code>
</p>
}
<div class="d-flex flex-wrap gap-2 justify-content-center">
<a asp-controller="Home" asp-action="Index" class="btn btn-primary">Go to Home</a>
<a href="javascript:history.back();" class="btn btn-info">Go Back</a>
<a asp-controller="Account" asp-action="Login" class="btn btn-danger">Switch Account</a>
</div>
@if (Env.IsDevelopment())
{
<hr class="my-4" />
<div class="accordion" id="debugAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="dbgHeading">
<button class="accordion-button collapsed text-danger" type="button"
data-bs-toggle="collapse" data-bs-target="#dbgPanel"
aria-expanded="false" aria-controls="dbgPanel">
Debug details (Development only)
</button>
</h2>
<div id="dbgPanel" class="accordion-collapse collapse" aria-labelledby="dbgHeading"
data-bs-parent="#debugAccordion">
<div class="accordion-body small">
<dl class="row mb-0">
<dt class="col-sm-4">User</dt>
<dd class="col-sm-8">@User.Identity?.Name</dd>
<dt class="col-sm-4">Authenticated</dt>
<dd class="col-sm-8">@User.Identity?.IsAuthenticated</dd>
<dt class="col-sm-4">Roles</dt>
<dd class="col-sm-8">
@{
var roles = User?.Claims
?.Where(c => c.Type == ClaimTypes.Role)
?.Select(c => c.Value)
?.ToArray() ?? Array.Empty<string>();
}
@((roles.Length == 0) ? "(none)" : string.Join(", ", roles))
</dd>
@if (isLocal)
{
<dt class="col-sm-4">Blocked URL</dt>
<dd class="col-sm-8">@returnUrl</dd>
}
</dl>
</div>
</div>
</div>
</div>
}
</div> 
</div> 
</div>
</div>
</div>
Why Role-Based Authorization in Views?

Role-based authorization in controllers protects server-side access, meaning even if someone tries to hit a URL directly, the [Authorize] attributes will block them. However, a good user experience also means:

  • You should hide buttons, menus, and links for features the user cannot access.
  • This avoids confusing the user with options they’re not allowed to use.
  • It prevents unnecessary “Access Denied” pages from being shown in standard navigation.

Example:

  • A “Customer” role user should not see an “Admin Dashboard” menu link at all.
  • If you don’t hide it in the view, they might click it and hit an Access Denied page, technically secure, but not friendly.

Bottom line:

  • Security is enforced in controllers.
  • Usability is improved by applying role checks in views.
How to Show or Hide the Navigation Menu Based on User Role?

ASP.NET Core MVC with Identity exposes user claims (including roles) to the User object in Razor views. You can use:

  • User.Identity.IsAuthenticated → Check if logged in.
  • User.IsInRole(“RoleName”) → Check if the user has a specific role.

You can use these to render HTML in your navigation layout conditionally.

Modifying the _Layout.cshtml View:

Please modify the _Layout.cshtml view as follows

@{
ViewData["Title"] = ViewData["Title"] ?? "Dot Net Tutorials";
bool isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
bool isAdmin = User?.IsInRole("Admin") ?? false;
bool isManager = User?.IsInRole("Manager") ?? false;
bool isStaff = isAdmin || isManager; // Admin OR Manager
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@ViewData["Title"] - Dot Net Tutorials</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
</head>
<body class="d-flex flex-column min-vh-100">
<!-- Dark Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand fw-bold" href="@Url.Action("Index", "Home")">Dot Net Tutorials</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent"
aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<!-- Left: Primary navigation -->
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="@Url.Action("Index", "Home")">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="@Url.Action("About", "Home")">About</a>
</li>
<!-- Any authenticated user -->
@if (isAuthenticated)
{
<li class="nav-item">
<a class="nav-link" asp-controller="Home" asp-action="SecureMethod">
Secure
</a>
</li>
}
<!-- Public -->
<li class="nav-item">
<a class="nav-link" asp-controller="Home" asp-action="NonSecureMethod">
Non Secure
</a>
</li>
<!-- Admin-only -->
@if (isAdmin)
{
<li class="nav-item">
<a class="nav-link" asp-controller="Roles" asp-action="Index">
Roles
</a>
</li>
}
<!-- Admin OR Manager -->
@if (isStaff)
{
<li class="nav-item">
<a class="nav-link" asp-controller="Users" asp-action="Index">
Users
</a>
</li>
}
</ul>
<!-- Right: Auth links / user menu -->
<ul class="navbar-nav mb-2 mb-lg-0">
@if (isAuthenticated)
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle me-1"></i>
Hello, @User!.Identity!.Name
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<a class="dropdown-item" href="@Url.Action("Profile", "Account")">
<i class="bi bi-person-lines-fill me-2"></i> Profile
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<form method="post" asp-controller="Account" asp-action="Logout" class="px-3 py-1">
<button type="submit" class="btn btn-link text-decoration-none w-100 text-start">
<i class="bi bi-box-arrow-right me-2"></i> Logout
</button>
</form>
</li>
</ul>
</li>
}
else
{
<li class="nav-item"><a class="nav-link" href="@Url.Action("Login", "Account")">Login</a></li>
<li class="nav-item"><a class="nav-link" href="@Url.Action("Register", "Account")">Register</a></li>
}
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="container flex-grow-1 py-3">
@RenderBody()
</main>
<!-- Footer -->
<footer class="bg-dark text-light py-4 mt-auto">
<div class="container d-flex flex-column flex-md-row justify-content-between align-items-center">
<div>
&copy; @DateTime.Now.Year Dot Net Tutorials. All rights reserved.
</div>
<div>
<a href="@Url.Action("Contact", "Home")" class="text-light me-3 text-decoration-none">Contact</a>
<a href="@Url.Action("Privacy", "Home")" class="text-light me-3 text-decoration-none">Privacy Policy</a>
<a href="@Url.Action("Terms", "Home")" class="text-light text-decoration-none">Terms of Service</a>
</div>
</div>
</footer>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Modifying the User Index Page:

Now, in the Users/Index view, we need to use Role-Based Authorization to make sure that certain action buttons like Create, Edit, Manage Roles, and Delete are only visible to users with specific roles.

  • Create userAdmin
  • EditAdmin or Manager
  • Manage RolesAdmin AND Manager
  • DeleteAdmin
  • DetailsAdmin or Manager

This only hides/shows buttons for a better UX. The controller should already have [Authorize] in place for security; this update is only to improve the user experience by hiding options the current user cannot access. So, please modify the Users/Index view as follows:

@using ASPNETCoreIdentityDemo.ViewModels
@using ASPNETCoreIdentityDemo.ViewModels.Users
@model PagedResult<UserListItemViewModel>
@{
ViewData["Title"] = "Users";
var filter = (UserListFilterViewModel)ViewBag.Filter;
bool isAdmin = User?.IsInRole("Admin") ?? false;
bool isManager = User?.IsInRole("Manager") ?? false;
bool isStaff = isAdmin || isManager;           // Admin OR Manager
bool canCreateUser = isAdmin;                  // Create = Admin
bool canEditUser = isStaff;                    // Edit = Admin OR Manager
bool canManageRoles = isAdmin && isManager;    // ManageRoles = Admin AND Manager
bool canDeleteUser = isAdmin;                  // Delete = Admin
bool canViewDetails = isStaff;                 // Details = Admin OR Manager
bool showActionsCol = canViewDetails || canEditUser || canManageRoles || canDeleteUser;
}
<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 (Admin only) *@
@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: Admin OR Manager *@
@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: Admin OR Manager *@
@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 Roles: Admin AND Manager *@
@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> Manage Roles
</a>
}
@* Delete: Admin only *@
@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>

Now, log in with a user who has only the Manager role and navigate to the User listing page, and you will see the following. You can see that the No Create, Delete, and Manage Roles button is visible.

Role-Based Authorization (RBAC) in ASP.NET Core Identity 

Summary:

Role-Based Authorization (RBAC) in ASP.NET Core Identity provides a structured and scalable way to manage access to application resources. By grouping permissions into roles and assigning those roles to users, we simplify access control, reduce errors, and improve security.

RBAC ensures that only authorized users can perform sensitive actions, making the system both safer and easier to maintain. Whether applied in controllers, actions, or views, role-based checks help enforce business rules consistently while keeping the application flexible and compliant with security best practices.

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

Leave a Reply

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