(27/30) Learning Blazor Together: Adding User and Claim Functionality

(27/30) Learning Blazor Together: Adding User and Claim Functionality

As mentioned earlier, `ASP.NET Core Identity` is based on `Claim` authentication, and `Role` is a `Claim` with type `Role`

Last updated 12/25/2021 11:08 AM
StrayaWorker
11 min read
Category
Blazor
Topic
Learning Blazor Together Series
Tags
.NET C# ASP.NET Core Blazor

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:

  1. Manage user claims in asp net core
  2. 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.

Keep Exploring

Related Reading

More Articles
Same category / Same tag 12/25/2021

(29/30)Learn Blazor Together: Blazor Unit Testing

The most boring part of developing a system is probably fixing bugs, especially errors like trying to access a null object (`Object reference not set to an instance of an object.`), which is the most common problem most people encounter when they first step into programming. To break free from the tedious bug-fixing process, this article introduces `unit testing`.

Continue Reading
Same category / Same tag 12/25/2021

(28/30) Learning Blazor Together: Policy-based Authorization

It was mentioned earlier that `ASP.NET Core Identity` uses `Claim`-based authentication. In fact, `ASP.NET Core Identity` has different types of authorization methods, the simplest being `Login Authorization`, `Role Authorization`, and `Claim Authorization`. However, all of the above are implemented in one way: Policy-based Authorization.

Continue Reading