Earlier it was mentioned that ASP.NET Core Identity is based on Claim verification, and Role is just a Claim with the type Role. In the era of ASP.NET Framework Identity, only Role verification existed. Claim was introduced in ASP.NET Core Identity to obtain third-party authorization from external programs such as Facebook, Twitter, etc., so that users don't have to register duplicate accounts on different platforms.
A Claim is actually just a combination of ClaimType and ClaimValue strings. It is usually not managed and assigned to User via a page like Role; instead, it is managed through the User page to add or remove Claims under a User. So today, we will implement the User page.
First, we need the ViewModel and data access layer. Since the tasks are the same, I won't elaborate.
The ViewModel for User:
namespace BlazorServer.ViewModels;
public class CustomUserViewModel
{
public CustomUserViewModel()
{
Claims = new List<string>();
}
public string? UserId { get; set; }
public string? UserName { get; set; }
public string? Email { get; set; }
public List<string>? Claims { get; set; }
}
The ViewModel that holds a single Claim:
namespace BlazorServer.ViewModels;
public class CustomUserClaimViewModel
{
public string? ClaimType { get; set; }
public bool IsSelected { get; set; }
}
The ViewModel that holds Claims under a User:
namespace BlazorServer.ViewModels;
public class CustomUserClaimsViewModel
{
public CustomUserClaimsViewModel()
{
Claims = new List<CustomUserClaimViewModel>();
}
public string? UserId { get; set; }
public List<CustomUserClaimViewModel> Claims { get; set; }
}
Since Claims are not registered by default like User, and unlike Role where users can define them, we first create several sets of Claims related to User permissions.
using System.Security.Claims;
namespace BlazorServer.Models;
public static class ClaimsStore
{
public static List<Claim> AllClaims = new()
{
new Claim("ManageUser", string.Empty),
new Claim("CreateUser", string.Empty),
new Claim("EditUser", string.Empty),
new Claim("DeleteUser", string.Empty)
};
}
The page IUserRepository:
using BlazorServer.Models;
using BlazorServer.ViewModels;
namespace BlazorServer.Repository;
public interface IUserRepository
{
Task<ResultViewModel> DeleteUserAsync(string userId);
Task<ResultViewModel> EditUserAsync(CustomUserViewModel model);
Task<CustomUserViewModel> GetUserAsync(string userId);
Task<List<CustomUserViewModel>> GetUsersAsync();
Task<CustomUserClaimsViewModel> EditClaimsInUserAsync(string userId);
Task<ResultViewModel> EditClaimsInUserAsync(CustomUserClaimsViewModel model);
}
Implementation of UserRepository. If you remember the RoleRepository.EditUsersInRoleAsyncPost method, we used two variables to store Role.Id and List<CustomUserRoleViewModel> model separately. Here, the Post method for editing Claims under a User is different from Role; it uses another ViewModel CustomUserClaimsViewModel to carry data, but the essence is the same.
using System.Security.Claims;
using BlazorServer.Models;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Identity;
namespace BlazorServer.Repository.Implement;
public class UserRepository : IUserRepository
{
private readonly UserManager<IdentityUser> _userManager;
public UserRepository(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
public async Task<List<CustomUserViewModel>> GetUsersAsync()
{
var customUsers = _userManager.Users.Select(user => new CustomUserViewModel
{ UserId = user.Id, UserName = user.UserName, Email = user.Email }).ToList();
return await Task.Run(() => customUsers);
}
public async Task<CustomUserViewModel> GetUserAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
var userClaims = await _userManager.GetClaimsAsync(user);
var result = new CustomUserViewModel
{
UserId = user.Id,
UserName = user.UserName,
Email = user.Email,
Claims = userClaims.Select(x => $"{x.Type} : {x.Value}").ToList()
};
return result;
}
public async Task<ResultViewModel> EditUserAsync(CustomUserViewModel model)
{
var user = await _userManager.FindByIdAsync(model.UserId);
if (user == null)
{
return new ResultViewModel
{
Message = $"Cannot find user with Id {model.UserId}",
IsSuccess = false
};
}
user.UserName = model.UserName;
user.Email = model.Email;
var result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return new ResultViewModel
{
Message = "User updated successfully!",
IsSuccess = true
};
}
return new ResultViewModel
{
Message = "User update failed!",
IsSuccess = false
};
}
public async Task<ResultViewModel> DeleteUserAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return new ResultViewModel
{
Message = $"Cannot find user with Id {userId}",
IsSuccess = false
};
}
var result = await _userManager.DeleteAsync(user);
if (result.Succeeded)
{
return new ResultViewModel
{
Message = "User deleted successfully!",
IsSuccess = true
};
}
return new ResultViewModel
{
Message = "User deletion failed!",
IsSuccess = false
};
}
public async Task<CustomUserClaimsViewModel> EditClaimsInUserAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
var claims = await _userManager.GetClaimsAsync(user);
var model = new CustomUserClaimsViewModel
{
UserId = userId
};
foreach (var claim in ClaimsStore.AllClaims)
{
var userClaim = new CustomUserClaimViewModel
{
ClaimType = claim.Type
};
if (claims.Any(c => c.Type == claim.Type && c.Value == "true"))
{
userClaim.IsSelected = true;
}
model.Claims.Add(userClaim);
}
return model;
}
public async Task<ResultViewModel> EditClaimsInUserAsync(CustomUserClaimsViewModel model)
{
var user = await _userManager.FindByIdAsync(model.UserId);
var claims = await _userManager.GetClaimsAsync(user);
var result = await _userManager.RemoveClaimsAsync(user, claims);
if (!result.Succeeded)
{
return new ResultViewModel
{
Message = "Unable to remove user's claims!",
IsSuccess = false
};
}
result = await _userManager.AddClaimsAsync(user,
model.Claims.Select(c => new Claim(c.ClaimType!, c.IsSelected ? "true" : "false")));
if (!result.Succeeded)
{
return new ResultViewModel
{
Message = "Unable to assign specified claims to user!",
IsSuccess = false
};
}
return new ResultViewModel
{
Message = "Claims assigned successfully",
IsSuccess = true
};
}
}
Then register in Program.cs:
builder.Services.AddScoped<IUserRepository, UserRepository>();
Now the frontend page presentation.
UserManagement.razor.cs:
using BlazorServer.Repository;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace BlazorServer.Pages.RolesManagement;
public partial class UserManagement
{
[Inject] protected IUserRepository? UserRepository { get; set; }
[Inject] protected NavigationManager? NavigationManager { get; set; }
[Inject] protected IJSRuntime? Js { get; set; }
private JsInteropClasses? _jsClass;
public List<CustomUserViewModel> Users { get; set; } = new();
protected override async Task OnInitializedAsync()
{
await LoadData();
_jsClass = new JsInteropClasses(Js!);
}
private async Task LoadData()
{
Users = await UserRepository!.GetUsersAsync();
}
private async Task EditUser(string userId)
{
NavigationManager!.NavigateTo($"UserManagement/EditUser/{userId}");
await Task.CompletedTask;
}
private async Task DeleteUser(string userId)
{
var sweetConfirm = new SweetConfirmViewModel()
{
RequestTitle = $"Are you sure you want to delete user {userId}?",
RequestText = "This operation cannot be undone",
ResponseTitle = "Deleted",
ResponseText = "User has been deleted",
};
var jsonString = JsonSerializer.Serialize(sweetConfirm);
var result = await _jsClass!.Confirm(jsonString);
if (result)
{
var deleted = await UserRepository!.DeleteUserAsync(userId);
if (deleted.IsSuccess)
{
await LoadData();
}
else
{
await _jsClass!.Alert(deleted.Message!);
}
}
}
}
UserManagement.razor:
@page "/UserManagement/UserList"
<h1>All Users</h1>
@if (Users.Any()) {
<NavLink
class="btn btn-primary mb-3"
href="Identity/Account/Register"
Match="NavLinkMatch.All"
>
Add User
</NavLink>
foreach (var user in Users) {
<div class="card mb-3 w-25">
<div class="card-header">User Id : @user.UserId</div>
<div class="card-body">
<h5 class="card-title">@user.UserName</h5>
</div>
<div class="card-footer">
<button
type="button"
class="btn btn-primary"
@onclick="() => EditUser(user.UserId)"
>
Edit User
</button>
<button
type="button"
class="btn btn-danger"
@onclick="() => DeleteUser(user.UserId)"
>
Delete User
</button>
</div>
</div>
} } else {
<div class="card w-25">
<div class="card-header">No users yet</div>
<div class="card-body">
<h5 class="card-title">Click the button below to add a new user</h5>
<NavLink
class="btn btn-primary"
href="Identity/Account/Register"
Match="NavLinkMatch.All"
>
Add User
</NavLink>
</div>
</div>
}
EditUser.razor.cs:
using BlazorServer.Repository;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
namespace BlazorServer.Pages.RolesManagement;
public partial class EditUser
{
[Inject] protected IUserRepository? UserRepository { get; set; }
[Inject] protected NavigationManager? NavigationManager { get; set; }
public CustomUserViewModel User { get; set; } = new();
[Parameter] public string? UserId { get; set; }
protected override async Task OnInitializedAsync()
{
var result = await UserRepository!.GetUserAsync(UserId!);
User = new CustomUserViewModel
{
UserId = result.UserId,
UserName = result.UserName,
Claims = result.Claims
};
}
private async Task EditRole()
{
await UserRepository!.EditUserAsync(User);
NavigationManager!.NavigateTo("/UserManagement/UserList");
}
public void EditUsersInRole()
{
NavigationManager!.NavigateTo($"/UserManagement/EditClaimsInUser/{UserId}");
}
public void Cancel()
{
NavigationManager!.NavigateTo($"/UserManagement/UserList");
}
}
EditUser.razor:
@page "/UserManagement/EditUser/{UserId}"
<EditForm class="mt-3" Model="User" OnValidSubmit="EditRole">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group row">
<label for="RoleName" class="col-sm-1 col-form-label">User Name</label>
<div class="col-sm-3">
<InputText
@bind-Value="User.UserName"
id="RoleName"
class="form-control"
placeholder="User Name"
></InputText>
</div>
</div>
<div class="card mb-3 w-50">
<div class="card-header">
<h3>Claims under this User</h3>
</div>
<div class="card-body">
@if (User.Claims.Any()) { foreach (var claim in User.Claims) {
<h5 class="card-title">@claim</h5>
} } else {
<h5 class="card-title">The user currently has no claims</h5>
}
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">Update User</button>
<button type="button" class="btn btn-info" @onclick="EditUsersInRole">
Add or Remove Claims under this User
</button>
<button type="button" class="btn btn-danger" @onclick="Cancel">
Cancel
</button>
</div>
</div>
</EditForm>
EditClaimsInUser.razor.cs:
using BlazorServer.Repository;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace BlazorServer.Pages.RolesManagement;
public partial class EditClaimsInUser
{
[Inject] protected IUserRepository? UserRepository { get; set; }
[Inject] protected NavigationManager? NavigationManager { get; set; }
[Inject] protected IJSRuntime? Js { get; set; }
private JsInteropClasses? _jsClass;
[Parameter] public string? UserId { get; set; }
public CustomUserClaimsViewModel UserClaimViewModel { get; set; } = new CustomUserClaimsViewModel();
protected override async Task OnInitializedAsync()
{
await LoadData();
_jsClass = new JsInteropClasses(Js!);
}
private async Task LoadData()
{
UserClaimViewModel = (await UserRepository!.EditClaimsInUserAsync(UserId!));
}
public async Task HandleValidSubmit()
{
var result = await UserRepository!.EditClaimsInUserAsync(UserClaimViewModel);
if (result.IsSuccess)
{
NavigationManager!.NavigateTo($"/UserManagement/EditUser/{UserId}");
}
else
{
await _jsClass!.Alert(result.Message!);
}
}
public void Cancel()
{
NavigationManager!.NavigateTo($"/UserManagement/EditUser/{UserId}");
}
}
EditClaimsInUser.razor:
@page "/UserManagement/EditClaimsInUser/{UserId}"
<EditForm Model="UserClaimViewModel" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="card">
<div class="card-header">
<h2>Add or Remove Claims from User</h2>
</div>
<div class="card-body">
@foreach (var claim in UserClaimViewModel.Claims) {
<div class="form-check m-1">
<label class="form-check-label">
<InputCheckbox @bind-Value="@claim.IsSelected"></InputCheckbox>
@claim.ClaimType
</label>
</div>
}
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">Update</button>
<button type="button" class="btn btn-danger" @onclick="@Cancel">
Cancel
</button>
</div>
</div>
</EditForm>
Finally, add the NavLink in NavMenu.razor:
<li class="nav-item px-3">
<NavLink
class="nav-link"
href="UserManagement/UserList"
Match="NavLinkMatch.All"
>
<span class="bi bi-people h4 p-2 mb-0" aria-hidden="true"></span> Users
</NavLink>
</li>
Now we have a simple User and Claim CRUD page.
References:
- Manage user claims in asp net core
- Claim type and claim value in claims policy based authorization in asp net core
Note: The code in this article is refactored using .NET 6 + Visual Studio 2022. You can compare the original link with the refactored code for learning. Thank you for reading and supporting the original author.