Back to: ASP.NET Core Identity Tutorials
Claims-Based Authorization in ASP.NET Core Identity
In this article, I will discuss how to implement Claims-Based Authorization in ASP.NET Core Identity. Please read our previous article discussing Role Claims Management in ASP.NET Core Identity.
In modern applications, authentication (who you are) is not enough. We also need authorization (what you can do). While Role-Based Authorization (RBAC) is useful for grouping permissions, it is often too rigid for real-world applications. This is where Claim-Based Authorization comes in.
Claims provide more flexible and extensible ways to control access by attaching attributes to users or roles. ASP.NET Core Identity integrates claims deeply, making them a first-class citizen for advanced authorization.
What is Claim-Based Authorization?
Claim-Based Authorization is an authorization mechanism in ASP.NET Core that makes access decisions based on claims assigned to a user (or inherited through a role).
Definition of a Claim:
A claim is a key–value pair that describes something about the user.
- Example: (Type = Permission, Value = AddUser)
- Example: (Type = Department, Value = Finance)
Unlike Role-Based Authorization (which groups users into buckets like Admin, Manager, Customer), Claim-Based Authorization is more flexible. It allows you to say:
- This user can EditUser but not DeleteUser
- This manager can approve the Budget, but not leave
So, claim-based authorization answers: What specific attributes, rights, or permissions does this user have?
So, roles answer who you are in the organization. On the other hand, claims answer what exactly you can do. Relying on roles alone becomes hard to scale as features grow, claims fill that gap, and can be mixed with roles when needed.
How Claim-Based Authorization Works in ASP.NET Core Identity?
Let’s break it down step by step. To understand better how Claim-Based Authorization Works in ASP.NET Core MVC with Identity, please have a look at the following diagram:
Authentication Creates Identity
When a user logs in, ASP.NET Core Identity authenticates them (e.g., via username/password).
- An ApplicationUser record is loaded from the database.
- A ClaimsPrincipal object is created to represent the logged-in user.
Claims Are Populated
ASP.NET Core Identity automatically loads User Claims and Role Claims during sign-in:
- User-specific claims come from the AspNetUserClaims (in our example, UserClaims) table.
- Role claims come from the AspNetRoleClaims (in our example, RoleClaims) table.
- Both are combined and added to the user’s ClaimsPrincipal.
Claims Become Part of Security Token
- Claims are stored in the authentication cookie (for cookie auth) or in the JWT token (for APIs).
- That means every request the user makes carries their claims.
Authorization Evaluates Claims
When a controller or action is protected, ASP.NET Core checks whether the user’s ClaimsPrincipal contains the required claim.
- If the claim exists → access granted.
- If not → 403 Forbidden or Access Denied page.
Enforcing Claim-Based Authorization in ASP.NET Core
Once claims are assigned to users or roles, they are stored inside the authenticated user’s ClaimsPrincipal object. To enforce claim-based authorization, your application must check whether the currently logged-in user has the required claim before granting access. In ASP.NET Core, there are two main enforcement approaches:
- Option 1: Without Policies (Direct Claim Checks)
- Option 2: With Policies (Centralized & Configured in Program.cs)
Here, we’ll focus only on Option 1 (Without Policies). In the next chapter, I will focus on the Policy-Based Approach.
Option 1: Without Policies (Direct Claim Checks)
This is the most straightforward approach. Instead of defining policies in the Program.cs, we can directly check user claims inside controllers, actions, or even Razor views.
When to Use
- Small applications or prototypes.
- Projects with only a handful of claim checks.
- When you want maximum control and flexibility right inside the controller or view.
How It Works
- ASP.NET Core automatically loads all user claims into the User object (ClaimsPrincipal) after authentication.
- You can check those claims using User.HasClaim(type, value).
- If the claim is missing, you return Forbid() (403) or redirect to a custom AccessDenied page.
Claim Checks in Controller Action
The action executes only if the logged-in user has the claim “Permission” : “AddUser”.
Claim Checks in Razor View
It is useful when you want to hide/show UI elements based on claims (e.g., only show “Export” button if the user has “Permission” : “ExportReports”).
Example to Understand Claim-Based Authorization in ASP.NET Core:
Now, please modify the Users Controller to implement Claims-Based Authorization in ASP.NET Core Identity. The following code is self-explanatory, so please read the comment lines for a better understanding.
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 [HttpGet] public async Task<IActionResult> Index([FromQuery] UserListFilterViewModel filter) { try { // Fetch paged users list from service using provided filter var result = await _userService.GetUsersAsync(filter); ViewBag.Filter = filter; // keep filter in ViewBag for persistence in UI return View(result); } catch (Exception ex) { // Log and show friendly error message _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 [HttpGet] public IActionResult Create() { // Permission check: Only users with "AddUser" claim can access if (!User.HasClaim("Permission", "AddUser")) return Forbid(); // returns 403 return View(new UserCreateViewModel()); } // POST: /Users/Create [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create(UserCreateViewModel model) { // Always check claims on POST too (defense-in-depth) if (!User.HasClaim("Permission", "AddUser")) return Forbid(); try { if (!ModelState.IsValid) return View(model); // redisplay form with validation errors // Call service to create user var (result, newId) = await _userService.CreateAsync(model); if (result.Succeeded) { SetSuccess($"User '{model.Email}' was created successfully."); return RedirectToAction(nameof(Index)); } // If Identity errors returned, push into ModelState for display AddIdentityErrors(result); return View(model); } catch (DbUpdateException dbx) { // Common DB error (e.g., duplicate key, constraint violation) _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) { // Catch any other unexpected errors _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} [HttpGet] public async Task<IActionResult> Edit(Guid id) { // Requires "EditUser" permission if (!User.HasClaim("Permission", "EditUser")) return Forbid(); 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 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(UserEditViewModel model) { if (!User.HasClaim("Permission", "EditUser")) return Forbid(); 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)); } // Handle concurrency errors specifically (optimistic concurrency check) 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."); } // Push other Identity errors into ModelState 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} [HttpGet] public async Task<IActionResult> Details(Guid id) { // Requires "ViewUsers" claim if (!User.HasClaim("Permission", "ViewUsers")) return Forbid(); 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} [HttpGet] public async Task<IActionResult> Delete(Guid id) { if (!User.HasClaim("Permission", "DeleteUser")) return Forbid(); 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 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> DeleteConfirmed(Guid id) { if (!User.HasClaim("Permission", "DeleteUser")) return Forbid(); 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)); } // Handle specific business rules encoded in IdentityResult.Errors 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} [HttpGet] public async Task<IActionResult> ManageRoles(Guid id) { if (!User.HasClaim("Permission", "ManageRoles")) return Forbid(); 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 [HttpPost, ValidateAntiForgeryToken] public async Task<IActionResult> ManageRoles(UserRolesEditViewModel model) { if (!User.HasClaim("Permission", "ManageRoles")) return Forbid(); if (!ModelState.IsValid) return View(model); try { // Collect only selected roles from form 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} - Admin only [Authorize(Roles = "Admin")] [HttpGet] public async Task<IActionResult> ManageClaims(Guid id) { // Role-based protection (only Admins can access) 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 - Admin only [Authorize(Roles = "Admin")] [HttpPost, ValidateAntiForgeryToken] 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 } }
Applying Claims Authorization in Views:
Now, let us apply the Claims Authorization on the Index view. Based on the Claims assigned to the User or Roles, we need to show and hide the buttons in the user Index Page. So, please modify the Index.cshtml View, which is inside the Views/Users folder, as follows.
@using ASPNETCoreIdentityDemo.ViewModels @using ASPNETCoreIdentityDemo.ViewModels.Users @model PagedResult<UserListItemViewModel> @{ ViewData["Title"] = "Users"; var filter = (UserListFilterViewModel)ViewBag.Filter; // Replace role checks with Claim checks // These check if the logged-in user has specific claims bool canCreateUser = User?.HasClaim("Permission", "AddUser") ?? false; bool canEditUser = User?.HasClaim("Permission", "EditUser") ?? false; bool canManageRoles = User?.HasClaim("Permission", "ManageRoles") ?? false; bool canDeleteUser = User?.HasClaim("Permission", "DeleteUser") ?? false; bool canViewDetails = User?.HasClaim("Permission", "ViewUsers") ?? false; //Admin Can Manage Claims, Its Intentionaly for Testing purpose bool canManageClaims = User?.IsInRole("Admin") ?? false; // bool canManageClaims = User?.HasClaim("Permission", "ManageUserClaims") ?? false; 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 User: Allowed only if user has "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"> @* View Details: Needs "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: Needs "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: Needs Admin Role only *@ @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: Needs "Permission=ManageRoles" *@ @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: Needs "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>
Now, run the application and it should work as expected.
Pros & Cons of This Approach
Pros
- Very direct and easy to understand.
- No setup required in Program.cs.
- Works well for small/simple applications.
Cons
- Scattered logic → checks spread across multiple controllers and views.
- Harder to maintain → as the app grows, tracking and updating claim checks becomes messy.
- Not DRY (Don’t Repeat Yourself) → the same claim logic may be repeated in multiple places.
Claim-Based Authorization in ASP.NET Core Identity provides a flexible way to secure modern applications by associating specific permissions with users or roles. Unlike role-based checks that can become difficult to scale, claims let us express exactly what a user can do. While inline claim checks in controllers and views are simple to implement for smaller projects, larger applications benefit from centralizing authorization logic to improve maintainability, consistency, and scalability.
In the next article, I will discuss Policy-Based Authorization in ASP.NET Core Identity. In this article, I explain how to implement Claims-Based Authorization in ASP.NET Core Identity. I hope you enjoy this article, Claims-Based Authorization in ASP.NET Core Identity.
what if we have more then 50 claims???, then we need to create 50 polices because each claim is using for different-different methods im i right or not ??????.
Want to see this in action?
I’ve created a step-by-step video on Claims-Based Authorization in ASP.NET Core Identity to help you understand the concepts more clearly.
👉 Watch it here: https://youtu.be/2O4PNzSprbA
Don’t forget to like, share, and subscribe for more ASP.NET Core tutorials!