Back to: ASP.NET Core Identity Tutorials
Role-Based Authorization (RBAC) in ASP.NET Core Identity
In this article, I will discuss Role-Based Authorization in ASP.NET Core Identity. Please 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:
- What is a Role?
- What is Role-Based Access Control (RBAC)?
- What are Authentication and Authorization?
- Why Do We Need Role-Based Authorization?
- What is Access Denied?
- How to Implement Role-Based Authorization in ASP.NET Core?
- What Happens When a User Tries to Access a Page They’re Not Authorized To?
- How to Configure Access Denied Page in ASP.NET Core MVC?
- Why Role-Based Authorization in Views?
- 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. A role is a logical representation of a category of users who share the same set of permissions or responsibilities within an application. Roles help group users based on access needs so that permissions can be granted to roles instead of individuals.
Example: Imagine a university:
- A Student can view courses and grades.
- A Teacher can manage assignments and grade students.
- An Admin can manage the whole system.
Each of these roles dictates what a person can or cannot do in the system.
Example: Think of a bank.
- Cashier role – can deposit/withdraw money for customers.
- Manager role – can approve loans and oversee staff.
- Customer role – can only deposit or withdraw their own money.
Instead of granting permissions to every individual, the bank assigns them to roles.
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.
- Roles are strings (e.g., “Admin”, “Editor”, ” Manager”, “Customer”) stored in a database and linked to users.
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. Let us understand Authentication from a layman’s point of view. For a better understanding, please have a look at the following image:
Imagine entering a secure office campus of an IT company. At the entrance, there is a biometric scanner that verifies employees before they can enter. Each employee must authenticate their identity by scanning their fingerprint or using another biometric method. The system then checks this biometric data against its stored records. If the fingerprint matches, the employee is allowed to enter the campus.
Inside the company, there are different rooms like Reception, HR Room, Accounts Section, Cafeteria, Server Room, and Admin Room. However, before accessing the campus itself, authentication is the first step, proving who you are to gain entry.
Similarly, in a web application or system, the user must provide credentials (like username/password or biometrics) that are validated before granting access. This verification process of confirming identity is what we call Authentication. To understand how authentication works, please have a look at the following diagram.
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. Let us understand Authorization from a layman’s point of view. For a better understanding, please have a look at the following image:
Imagine an IT company building with restricted access to different rooms such as Reception, HR Room, Accounts Section, Cafeteria, Server Room, and Admin Room. After an employee verifies their identity through biometrics at the entrance (authentication), they enter the campus.
However, not every employee has access to all areas. For example:
- Employee 1 can move freely between the Reception and Cafeteria, but does not have access to more sensitive areas.
- Employee 2 has permission to access the Accounts Section and the Admin Room, which may have more confidential information.
These permissions are based on the employee’s role and the privileges assigned to them. This process of deciding what specific areas or resources an authenticated employee can access is called Authorization.
Similarly, in web applications, authorization mechanisms ensure that authenticated users can only access certain features or data based on their roles and permissions, protecting sensitive resources from unauthorized use. To understand how authorization works, please have a look at the following diagram.
What is Role-Based Access Control (RBAC)?
RBAC (which means controlling who can do what) is a security mechanism that restricts system access based on the roles of individual users within an organization. Rather than assigning permissions directly to users, permissions are assigned to roles, and then users are assigned to those roles.
How RBAC Works:
- Define Roles → Admin, Manager, User.
- Assign Roles to Users → John = Admin, Sara = User.
- Check Roles Before Access → If John tries to open the Admin Dashboard → Allowed.
If Sara tries → Access Denied.
Example: Library
A library has Members and Librarians. Members can borrow books, while librarians can add/remove them. The software checks your role each time you try to perform an action.
Example: In a hospital
- Doctors can view and update patient records.
- Nurses can update observations but cannot prescribe medicine.
- Receptionists can only register patients.
RBAC ensures no receptionist accidentally prescribes medicine, and no nurse approves a surgery.
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.
Role-based authorization is essential for:
- Security: Ensures users cannot access sensitive features not meant for them.
- Maintainability: Easier to manage permissions via roles instead of setting them individually per user.
- Scalability: As the app grows, roles can be updated to manage new permissions centrally.
- Compliance: Many industries (banking, healthcare) require strict access control.
Without RBAC, manually managing access permissions becomes error-prone and unmanageable as the number of users grows.
Example:
A company has 100 employees. Assigning permissions one-by-one is inefficient. With RBAC, you assign employees to roles like “HR”, “Finance”, or “IT”, and each role carries the required access, making life easier and more secure.
What is Access Denied?
An Access Denied situation occurs when a user is authenticated (logged in) but not authorized (lacks the required role or permission) to perform a certain action or access a specific resource or page. The system recognizes the user but restricts their actions to safeguard sensitive data or operations.
Example: If you have a basic Netflix plan, you cannot watch Ultra HD. Even though you are a valid subscriber (authenticated), the system tells you, “Sorry, this plan doesn’t allow HD.” That’s Access Denied.
How to Implement Role-Based Authorization in ASP.NET Core?
RBAC implementation in ASP.NET Core involves the following steps:
- Define Roles: You can seed roles like Admin, Manager, User into the database using Identity.
- Assign Roles to Users: During registration or via the admin panel, users are assigned one or more roles.
- Restrict Access Using Attributes: Use [Authorize(Roles = “Admin”)] on controllers or actions to protect them.
- Policy-Based (Optional): For more flexibility, you can create authorization policies that combine roles and conditions.
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 (AND): Admin and Manager
- ManageRoles (GET/POST): Multiple roles (OR): Admin or 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>()); // empty model to avoid null view } } // 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 (AND): Both Admin and Manager [Authorize(Roles = "Admin")] [Authorize(Roles = "Manager")] // AND overall [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 (AND): Both Admin and Manager [Authorize(Roles = "Admin")] [Authorize(Roles = "Manager")] // AND overall [HttpPost] [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)); } // 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 (OR): Admin or Manager [Authorize(Roles = "Admin,Manager")] // OR semantics [HttpGet] 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 (OR): Admin or Manager [Authorize(Roles = "Admin,Manager")] // OR semantics [HttpPost, ValidateAntiForgeryToken] 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 }); } // 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.
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> © @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 user → Admin
- Edit → Admin and Manager
- Manage Roles → Admin or Manager
- Delete → Admin
- Details → Admin 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 = isAdmin && isManager; // Edit = Admin AND Manager bool canManageRoles = isStaff; // ManageRoles = Admin OR 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 AND 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 OR 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.
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 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.
Want to see Role-Based Authorization (RBAC) in ASP.NET Core Identity in action?
We’ve created a detailed video tutorial that walks you step by step through the concepts, implementation, and best practices.
👉 Watch the full video here: https://youtu.be/dQDmf7jOALM
🎥 This video complements our article with real-world examples and demonstrations to help you understand RBAC in ASP.NET Core more effectively. Don’t forget to subscribe to our channel, Dot Net Tutorials, for more in-depth tutorials.