Back to: ASP.NET Core Identity Tutorials
Manage User Claims in ASP.NET Core Identity
In this article, I will discuss managing user claims, i.e., how to add or remove user claims in ASP.NET Core Identity. Please read our previous article discussing Claims Management in ASP.NET Core Identity. User claims in ASP.NET Core Identity allow us to assign specific rights or attributes directly to a user, going beyond role-based authorization. Their main objective is to provide more control over what a user can access or perform in the application.
What are User Claims in ASP.NET Core Identity?
In ASP.NET Core Identity, User Claims are pieces of information about a user that describe their identity in a structured way. A claim is essentially a key–value pair that represents something the application knows about the user, such as their email, department, age, or a specific permission.
Unlike roles, which group users under predefined categories, claims provide a more flexible way to define attributes and permissions. For example, one user might have a claim like Department = HR or CanApproveInvoices = true, which helps the application make more contextual authorization decisions.
Why We Need User Claims?
We need User Claims because real-world applications often require more flexibility than roles alone can provide. Roles answer “What group does the user belong to?”, while claims answer “What can this user specifically do?” or “What attributes describe this user?”.
For example, two users may both belong to the Manager role, but one may have the claim Permission = ApproveBudget, while another has Permission = ApproveLeave. Claims enable policy-based authorization, dynamic access control, and personalization. In short, they provide a scalable and extensible way to handle user rights and attributes beyond simple role membership.
Pages to Update and Create:
Let us proceed and understand how to Manage User Claims in our existing application. We will update a few existing pages and add a new page for user claims management. Let us first see the pages to be developed or enhanced.
User Index Page (Existing Page):
Now, we need to enhance the User Index Page to include a link to manage the User Claims. So, the updated User Index Page will look like the one below.
User Details Page (Existing Page):
Now, we need to enhance the User Details Page to include a section to display all the assigned User Claims. So, the updated User Details Page will look like the one below.
Manage User Claims Page (New Page):
Now, we will create a new Page to manage the user claims. From this page, we can add new claims, we can remove existing claims.
Adding and Updating ViewModels
Let us proceed and add/update the view models to manage the user claims.
Modify UserDetailsViewModel
Add a property to display assigned user claims in the User Details page. So, please modify the UserDetailsViewModel.cs class file, which is available inside ViewModels/Users folder, as follows. The UserDetailsViewModel holds the complete profile data for the details page.
namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserDetailsViewModel { public Guid Id { get; init; } public string Email { get; init; } = string.Empty; public string UserName { get; init; } = string.Empty; public string FirstName { get; init; } = string.Empty; public string? LastName { get; init; } public string? PhoneNumber { get; init; } public DateTime? DateOfBirth { get; init; } public DateTime? LastLogin { get; init; } public bool IsActive { get; init; } public bool EmailConfirmed { get; init; } public DateTime? CreatedOn { get; init; } public DateTime? ModifiedOn { get; init; } public List<string> Roles { get; init; } = new(); // e.g., ["Permission: ViewUsers", "Permission: EditUser"] public List<string> Claims { get; init; } = new(); } }
UserClaimCheckboxItem
Create a class file named UserClaimCheckboxItem.cs within the ViewModels/Users folder, then copy and paste the following code. The UserClaimCheckboxItem is a lightweight DTO for the Manage Claims UI; it represents one claim row (id, type, value, category, description) and whether it’s currently selected for the user.
namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserClaimCheckboxItem { // Primary key of ClaimMaster public Guid ClaimId { get; set; } public string ClaimType { get; set; } = string.Empty; // e.g., "Permission" public string ClaimValue { get; set; } = string.Empty; // e.g., "ViewUsers" public string Category { get; set; } = string.Empty; //User, Both public string? Description { get; set; } // Optional admin help text public bool IsSelected { get; set; } // Whether the user currently has it } }
UserClaimsEditViewModel
Create a class file named UserClaimsEditViewModel.cs within the ViewModels/Users folder, then copy and paste the following code. The UserClaimsEditViewModel is the form model for the Manage Claims page; it carries the target UserId, UserName, and the collection of ClaimCheckboxItem entries to render and post back.
using System.ComponentModel.DataAnnotations; namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserClaimsEditViewModel { [Required(ErrorMessage = "Invalid user.")] public Guid UserId { get; set; } [Display(Name = "User Name")] public string UserName { get; set; } = string.Empty; // Only claims from Category = User or Both (and IsActive) public List<UserClaimCheckboxItem> Claims { get; set; } = new(); } }
Service interface
The IUserService interface is extended with two methods, GetClaimsForEditAsync and UpdateClaimsAsync, to build the claims editor view model and to persist changes to the user’s claims. So, please modify the IUserService interfaces as follows.
using ASPNETCoreIdentityDemo.ViewModels; using ASPNETCoreIdentityDemo.ViewModels.Users; using Microsoft.AspNetCore.Identity; namespace ASPNETCoreIdentityDemo.Services { public interface IUserService { Task<PagedResult<UserListItemViewModel>> GetUsersAsync(UserListFilterViewModel filter); Task<(IdentityResult Result, Guid? UserId)> CreateAsync(UserCreateViewModel model); Task<UserEditViewModel?> GetForEditAsync(Guid id); Task<IdentityResult> UpdateAsync(UserEditViewModel model); Task<UserDetailsViewModel?> GetDetailsAsync(Guid id); Task<IdentityResult> DeleteAsync(Guid id); Task<UserRolesEditViewModel?> GetRolesForEditAsync(Guid userId); Task<IdentityResult> UpdateRolesAsync(Guid userId, IEnumerable<Guid> selectedRoleIds); //New Methods for User Claim Management Task<UserClaimsEditViewModel?> GetClaimsForEditAsync(Guid userId); Task<IdentityResult> UpdateClaimsAsync(Guid userId, IEnumerable<Guid> selectedClaimIds); } }
Service implementation
Open the Services/UserService.cs class file and add the following two methods. These two methods are used to manage the User Claims.
GetClaimsForEditAsync:
The GetClaimsForEditAsync(…) method queries active ClaimMasters limited to categories User/Both, reads the user’s current claims, and returns a UserClaimsEditViewModel with items pre-checked where the user already has them.
public async Task<UserClaimsEditViewModel?> GetClaimsForEditAsync(Guid userId) { var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId); if (user == null) return null; // Get all active claims that can be assigned to Users or Both var allClaims = await _dbContext.ClaimMasters .AsNoTracking() .Where(c => c.IsActive && (c.Category == "User" || c.Category == "Both")) .OrderBy(c => c.ClaimType).ThenBy(c => c.ClaimValue) .ToListAsync(); // Read current user claims from Identity var currentClaims = await _userManager.GetClaimsAsync(user); var vm = new UserClaimsEditViewModel { UserId = user.Id, UserName = user.UserName!, Claims = allClaims.Select(c => new UserClaimCheckboxItem { ClaimId = c.Id, ClaimType = c.ClaimType, ClaimValue = c.ClaimValue, Category = c.Category, Description = c.Description, IsSelected = currentClaims.Any(uc => uc.Type == c.ClaimType && uc.Value == c.ClaimValue) }).ToList() }; return vm; }
UpdateClaimsAsync
The UpdateClaimsAsync(…) method validates selected claim IDs against allowed ClaimMasters, removes existing user claims, and adds the newly selected claims via UserManager.
public async Task<IdentityResult> UpdateClaimsAsync(Guid userId, IEnumerable<Guid> selectedClaimIds) { var user = await _userManager.FindByIdAsync(userId.ToString()); if (user == null) return IdentityResult.Failed(new IdentityError { Code = "UserNotFound", Description = "User not found." }); // Only allow choosing from active ClaimMasters in Category = User or Both var allowedClaims = await _dbContext.ClaimMasters .Where(c => c.IsActive && (c.Category == "User" || c.Category == "Both")) .ToListAsync(); //Selected Claims var selected = allowedClaims.Where(c => selectedClaimIds.Contains(c.Id)).ToList(); //Current Claims var currentClaims = await _userManager.GetClaimsAsync(user); // Remove old foreach (var claim in currentClaims) { await _userManager.RemoveClaimAsync(user, claim); } // Add selected foreach (var claim in selected) { await _userManager.AddClaimAsync(user, new Claim(claim.ClaimType, claim.ClaimValue)); } return IdentityResult.Success; }
Importing Namespace:
Please import the following namespace.
Using System.Security.Claims;
Modify the GetDetailsAsync method of the User Service class:
Please modify the GetDetailsAsync method of the User Service class as follows to return the assigned claims of the user.
// Returns detailed view model including assigned roles. public async Task<UserDetailsViewModel?> GetDetailsAsync(Guid id) { // Read-only entity for display var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); if (user == null) return null; // Identity API requires the user entity for role lookup var roles = await _userManager.GetRolesAsync(user); var claims = await _userManager.GetClaimsAsync(user); var claimTexts = claims .OrderBy(c => c.Type).ThenBy(c => c.Value) .Select(c => $"{c.Type}: {c.Value}") .ToList(); return new UserDetailsViewModel { Id = user.Id, Email = user.Email!, UserName = user.UserName!, FirstName = user.FirstName, LastName = user.LastName, PhoneNumber = user.PhoneNumber, DateOfBirth = user.DateOfBirth, LastLogin = user.LastLogin, IsActive = user.IsActive, EmailConfirmed = user.EmailConfirmed, CreatedOn = user.CreatedOn, ModifiedOn = user.ModifiedOn, Roles = roles.OrderBy(r => r).ToList(), Claims = claimTexts }; }
Modifying UsersController
Please add the following two methods to the Users controller.
ManageClaims (GET):
This method loads the claims editor view model for a given user; if the user isn’t found, it sets an error and redirects back to the index.
[Authorize(Roles = "Admin")] [HttpGet] public async Task<IActionResult> ManageClaims(Guid id) { var userClaimsEditViewModel = await _userService.GetClaimsForEditAsync(id); if (userClaimsEditViewModel == null) { SetError("The user was not found."); return RedirectToAction(nameof(Index)); } return View(userClaimsEditViewModel); }
ManageClaims (POST):
This method accepts the posted UserClaimsEditViewModel, extracts the selected claims, calls the service to update, and then shows success or validation errors before redirecting to the user’s details page.
[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 }); } // Friendly messages for validation-style failures 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); } }
Creating and Updating Views:
We will update a few existing views and create a new one to manage user claims in ASP.NET Core Identity.
Users/Index.cshtml — Add Manage Claims button next to Manage Roles:
Please modify the User Index view as follows. It enhances the user’s grid by adding a Claims action button (next to existing actions) so admins can jump directly to Manage Claims for any user.
@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 canManageClaims = isAdmin; // ManageClaims = Admin 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 Claim: Admin 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: 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> 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>
Users/Details.cshtml — Add Claims card below the Roles & Permissions Card
Right after the existing Roles card (which has a header “Roles & Permissions” and a Manage button), we need to add the Claim section. Please modify the User Details view as follows.
@model ASPNETCoreIdentityDemo.ViewModels.Users.UserDetailsViewModel @{ ViewData["Title"] = "User Details"; string fullName = $"{Model.FirstName} {Model.LastName}".Trim(); string initials = (!string.IsNullOrWhiteSpace(Model.FirstName) ? Model.FirstName[0].ToString() : "") + (!string.IsNullOrWhiteSpace(Model.LastName) ? Model.LastName[0].ToString() : ""); string activeBadge = Model.IsActive ? "bg-success" : "bg-secondary"; string emailBadge = Model.EmailConfirmed ? "bg-success" : "bg-warning text-dark"; } <div class="container mt-1"> <!-- Page header --> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3 pb-2 border-bottom"> <div> <h2 class="h4 fw-bold text-primary mb-1"> User Profile: @(!string.IsNullOrWhiteSpace(fullName) ? fullName : Model.UserName) </h2> <p class="text-muted small mb-0">Review account details, health, roles, and claims.</p> </div> <div class="d-flex gap-2"> <a asp-controller="Users" asp-action="Index" class="btn btn-info"> <i class="bi bi-arrow-left me-1"></i> Back to Users </a> <a asp-controller="Users" asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-success"> <i class="bi bi-pencil-square me-1"></i> Edit User </a> <a asp-controller="Users" asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-danger"> <i class="bi bi-pencil-square me-1"></i> Delete User </a> </div> </div> <!-- Alerts --> @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> } <div class="row g-3"> <!-- Left: Profile & Info --> <div class="col-lg-7"> <div class="card border-0 shadow h-100"> <div class="card-header bg-dark text-white"> <div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center gap-3"> <div class="rounded-circle bg-secondary d-inline-flex align-items-center justify-content-center text-white fw-bold" style="width:52px;height:52px;"> @initials.ToUpper() </div> <div> <div class="h5 mb-0">@(!string.IsNullOrWhiteSpace(fullName) ? fullName : Model.UserName)</div> <small class="text-white-50">@Model.UserName</small> </div> </div> <div class="d-flex gap-2"> <span class="badge rounded-pill @activeBadge"> @(Model.IsActive ? "Active" : "Inactive") </span> <span class="badge rounded-pill @emailBadge"> @(Model.EmailConfirmed ? "Email Confirmed" : "Email Unconfirmed") </span> </div> </div> </div> <div class="card-body"> <!-- Contact --> <div class="mb-2"> <div class="small text-uppercase text-muted mb-2">Contact</div> <ul class="list-group list-group-flush"> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"> <i class="bi bi-envelope me-2"></i>Email </span> <span class="d-flex align-items-center gap-2"> <a href="mailto:@Model.Email" class="link-dark text-decoration-none">@Model.Email</a> <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="tooltip" data-bs-title="Copy email" onclick="navigator.clipboard.writeText('@Model.Email')"> <i class="bi bi-clipboard"></i> </button> </span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-telephone me-2"></i>Phone</span> <span>@(string.IsNullOrWhiteSpace(Model.PhoneNumber) ? "-" : Model.PhoneNumber)</span> </li> </ul> </div> <!-- Account --> <div class="mt-4"> <div class="small text-uppercase text-muted mb-2">Account</div> <ul class="list-group list-group-flush"> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-cake2 me-2"></i>Date of Birth</span> <span>@(Model.DateOfBirth?.ToString("yyyy-MM-dd") ?? "-")</span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-box-arrow-in-right me-2"></i>Last Login</span> <span>@(Model.LastLogin?.ToString("yyyy-MM-dd HH:mm") ?? "Never")</span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-calendar2-plus me-2"></i>Created</span> <span>@(Model.CreatedOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")</span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-calendar2-check me-2"></i>Modified</span> <span>@(Model.ModifiedOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")</span> </li> </ul> </div> </div> </div> </div> <!-- Right: Roles + Claims stacked --> <div class="col-lg-5 d-flex flex-column"> <!-- Roles --> <div class="card border-0 shadow flex-fill mb-2"> <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center"> <span class="fw-semibold">Roles & Permissions</span> <a asp-action="ManageRoles" asp-route-id="@Model.Id" class="btn btn-sm btn-success"> <i class="bi bi-people me-1"></i> Manage Roles </a> </div> <div class="card-body overflow-auto"> @if (Model.Roles == null || Model.Roles.Count == 0) { <div class="text-center text-muted py-3"> <i class="bi bi-shield-lock mb-2" style="font-size:1.4rem;"></i> <div>No roles assigned</div> <div class="mt-2"> <a asp-action="ManageRoles" asp-route-id="@Model.Id" class="btn btn-success">Assign Roles</a> </div> </div> } else { <div class="d-flex flex-wrap gap-2"> @foreach (var role in Model.Roles.OrderBy(x => x)) { <span class="badge rounded-pill bg-primary fs-6 px-3 py-2"> <i class="bi bi-shield-check me-1"></i>@role </span> } </div> } </div> </div> <!-- Claims --> <div class="card border-0 shadow flex-fill"> <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center"> <span class="fw-semibold">User Claims</span> <a asp-action="ManageClaims" asp-route-id="@Model.Id" class="btn btn-sm btn-success"> <i class="bi bi-key me-1"></i> Manage Claims </a> </div> <div class="card-body overflow-auto"> @if (Model.Claims == null || Model.Claims.Count == 0) { <div class="text-center text-muted py-3"> <i class="bi bi-key mb-2" style="font-size:1.4rem;"></i> <div>No user claims assigned</div> <div class="mt-2"> <a asp-action="ManageClaims" asp-route-id="@Model.Id" class="btn btn-sm btn-success">Assign Claims</a> </div> </div> } else { <div class="d-flex flex-wrap gap-2"> @foreach (var claim in Model.Claims.OrderBy(x => x)) { <span class="badge rounded-pill bg-primary fs-6 px-3 py-2"> <i class="bi bi-check2-circle me-1"></i>@claim </span> } </div> } </div> </div> </div> </div> </div> @section Scripts { <script> document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el)); </script> }
Create a new ManageClaims View
Create a view named ManageClaims.cshtml within the Views/Users folder, then copy and paste the following code. This page lists assignable claims as checkboxes, shows total/selected counters, supports Select All / Clear All, and posts back to save the user’s claims.
@using ASPNETCoreIdentityDemo.ViewModels.Users @model UserClaimsEditViewModel @{ ViewData["Title"] = "Manage User Claims"; } <div class="container mt-2"> <div class="mx-auto" style="max-width: 1100px;"> <!-- Page header --> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3 pb-2 border-bottom"> <div> <h1 class="h4 fw-bold text-primary mb-1"> Manage Claims for <span class="text-dark">@Model.UserName</span> </h1> <p class="text-muted small mb-0"> Assign or remove claims. Only <em>User</em> and <em>Both</em> category claims are shown. </p> </div> <!-- Right side buttons --> <div class="d-flex gap-2"> <a asp-action="Index" class="btn btn-primary"> <i class="bi bi-arrow-left me-1"></i> Back to Users </a> <a asp-action="Details" asp-route-id="@Model.UserId" class="btn btn-success"> <i class="bi bi-arrow-left me-1"></i> Back to Details </a> </div> </div> <div class="card shadow border-0 rounded"> <!-- Header with actions --> <div class="card-header bg-dark text-white d-flex align-items-center justify-content-between flex-wrap gap-2"> <div class="d-flex align-items-center gap-4"> <span class="fw-semibold fs-5"><i class="bi bi-shield-check me-2"></i>Claim Assignments</span> <div class="d-flex gap-3 align-items-center"> <span class="badge bg-info text-dark fs-6 px-3 py-2 shadow-sm rounded-pill"> <i class="bi bi-list-check me-1"></i>Total: @Model.Claims.Count </span> <span class="badge bg-success fs-6 px-3 py-2 shadow-sm rounded-pill" id="selectedCount"> <i class="bi bi-check-circle-fill me-1"></i>Selected: @(Model.Claims.Count(c => c.IsSelected)) </span> </div> </div> <div class="d-flex gap-2"> <button type="button" class="btn btn-success btn-sm px-3 shadow-sm" id="selectAllBtn"> <i class="bi bi-check2-square me-1"></i>Select All </button> <button type="button" class="btn btn-warning btn-sm px-3 shadow-sm text-dark" id="clearAllBtn"> <i class="bi bi-x-circle me-1"></i>Clear All </button> </div> </div> <div class="card-body"> <form asp-action="ManageClaims" method="post" id="claimsForm"> @Html.AntiForgeryToken() <input type="hidden" asp-for="UserId" /> <input type="hidden" asp-for="UserName" /> @if (!ViewData.ModelState.IsValid && ViewData.ModelState.Values.Any(v => v.Errors.Count > 0)) { <div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="fw-semibold mb-1">We couldn’t save your changes.</div> <div class="small text-muted mb-2">Please review the messages below and try again.</div> <div asp-validation-summary="All"></div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <!-- Claims list in two columns --> <div class="row" id="claimsList"> @for (int i = 0; i < Model.Claims.Count; i++) { var chkId = $"claim_{i}"; var type = Model.Claims[i].ClaimType ?? ""; var value = Model.Claims[i].ClaimValue ?? ""; var desc = Model.Claims[i].Description ?? ""; <div class="col-md-6 col-lg-6 mb-3"> <label class="list-group-item rounded shadow-sm border"> <div class="d-flex align-items-start"> <input asp-for="Claims[i].IsSelected" class="form-check-input me-2 mt-1" id="@chkId" /> <div class="flex-grow-1"> <div class="fw-semibold">@type : @value</div> <div class="text-muted small"> @(string.IsNullOrWhiteSpace(desc) ? "No description available" : desc) </div> </div> </div> <!-- hidden fields --> <input asp-for="Claims[i].ClaimId" type="hidden" /> <input asp-for="Claims[i].ClaimType" type="hidden" /> <input asp-for="Claims[i].ClaimValue" type="hidden" /> <input asp-for="Claims[i].Description" type="hidden" /> </label> </div> } </div> <!-- Actions --> <div class="d-flex justify-content-end gap-2 mt-1"> <button type="submit" class="btn btn-primary px-4"> <i class="bi bi-save me-1"></i> Save Claims </button> <a asp-action="Details" asp-route-id="@Model.UserId" class="btn btn-info px-4"> Cancel </a> </div> </form> </div> <div class="card-footer bg-light small text-muted"> Tip: “Select all” and “Clear all” apply to every claim shown on this page. </div> </div> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> <script> (function ($) { const $list = $('#claimsList'); const $selectAllBtn = $('#selectAllBtn'); const $clearAllBtn = $('#clearAllBtn'); const $selectedCount = $('#selectedCount'); function updateSelectedCount() { const count = $list.find('input[type="checkbox"]:checked').length; $selectedCount.text('Selected: ' + count); } $selectAllBtn.on('click', function () { $list.find('input[type="checkbox"]').prop('checked', true); updateSelectedCount(); }); $clearAllBtn.on('click', function () { $list.find('input[type="checkbox"]').prop('checked', false); updateSelectedCount(); }); $list.on('change', 'input[type="checkbox"]', function () { updateSelectedCount(); }); updateSelectedCount(); })(jQuery); </script> }
Now, run the application and test its functionalities; it should work as expected. The user-specific claims will be stored in the UserClaims table.
Managing user claims in ASP.NET Core Identity bridges the gap between simple role-based authorization and highly customized user-specific policies. With proper claim management in place, developers and administrators can ensure a secure, maintainable, and user-centric identity solution that enhances both application security and overall user experience.
In the next article, I will discuss how to Manage Role Claims in ASP.NET Core Identity. In this article, I explain how to add or Remove User Claims in ASP.NET Core Identity. I hope you enjoy this article, Manage User Claims in ASP.NET Core Identity.
📺 Want to see this in action?
I’ve created a detailed step-by-step video on Managing User Claims in ASP.NET Core Identity. You can watch it here: 👉 https://youtu.be/LNT2ftn2x1o
This video will help you understand the concepts more clearly with a real-time demo. Don’t forget to like, share, and subscribe for more ASP.NET Core tutorials!