5/16/2020 Admin

Implementing Roles In Blazor WebAssembly


image

You can implement roles in a Blazor WebAssembly application.

Install Blazor WebAssembly

image

Blazor has two modes, Blazor Server and Blazor WebAssembly.

For this tutorial, install the latest Blazor WebAssembly version.

Create The Application

image

Open Visual Studio.

image

Create a New Project.

image

Create a Blazor App.

image

Give the project the name RoleEnabledWebAssembly and click Create.

image

Select the Blazor WebAssembly template, select ASP.NET Core hosted and click the change link under Authentication.

image

Select Individual User Accounts and click OK.

image

Click Create.

image

The application will be created.

image

The application will display in Visual Studio.

Enable Role Support

image

Open the Startup.cs file in the Server project.

Change this code:

								
									services.AddDefaultIdentity<ApplicationUser>(options =>
								
									options.SignIn.RequireConfirmedAccount =
									true)
								
									.AddEntityFrameworkStores<ApplicationDbContext>();
								

								

								
									services.AddIdentityServer()
								
									.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
								

							

To this:

								
									services.AddDefaultIdentity<ApplicationUser>()
								
									.AddRoles<IdentityRole>()
								
									.AddEntityFrameworkStores<ApplicationDbContext>();
								

								

								
									// Configure identity server to put the role claim into the id token
									
								
									// and the access token and prevent the default mapping for roles
									
								
									// in the JwtSecurityTokenHandler.
								
									services.AddIdentityServer()
								
									.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
								
									options.IdentityResources["openid"].UserClaims.Add("role");
								
									options.ApiResources.Single().UserClaims.Add("role");
								
									});
								

								

								
									// Need to do this as it maps "role" to ClaimTypes.Role and causes issues
								
									System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler
								
									.DefaultInboundClaimTypeMap.Remove("role");
								

							

image

Blazor dynamically creates a Register page, but, we need to override it so that we can add code that will:

  1. Create an Administrators role (if one does not already exist)
  2. Automatically add any user named Admin@Email to the Administrators role

Add an Account folder to the Areas/Identity/Pages folder.

Add the following files:

_ViewImports.cshtml

								
@using Microsoft.AspNetCore.Identity
								
@using RoleEnabledWebAssembly.Server.Areas.Identity
								
@using RoleEnabledWebAssembly.Server.Areas.Identity.Pages
								
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
								

							

Register.cshtml

								
@page
								
@model RegisterModel
								
@{
								
									ViewData["Title"] = "Register";
								
}
								
<h1>@ViewData["Title"]</h1>
								
<div
									class="row">
								
									<div
									class="col-md-4">
								
									<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
								
									<h4>Create a
									new
									account.</h4>
								
									<hr />
								
									<div asp-validation-summary="All"
									class="text-danger"></div>
								
									<div
									class="form-group">
								
									<label asp-for="Input.Email"></label>
								
									<input asp-for="Input.Email"
									class="form-control" />
								
									<span asp-validation-for="Input.Email"
									class="text-danger"></span>
								
									</div>
								
									<div
									class="form-group">
								
									<label asp-for="Input.Password"></label>
								
									<input asp-for="Input.Password"
									class="form-control" />
								
									<span asp-validation-for="Input.Password"
									class="text-danger"></span>
								
									</div>
								
									<div
									class="form-group">
								
									<label asp-for="Input.ConfirmPassword"></label>
								
									<input asp-for="Input.ConfirmPassword"
									class="form-control" />
								
									<span asp-validation-for="Input.ConfirmPassword"
									class="text-danger"></span>
								
									</div>
								
									<button type="submit"
									class="btn btn-primary">Register</button>
								
									</form>
								
									</div>
								
									<div
									class="col-md-6 col-md-offset-2">
								
								
									</div>
								
</div>
								
@section Scripts {
								
									<partial name="_ValidationScriptsPartial" />
								
}
								

							

Register.cshtml.cs

								
									using
									System;
								
									using
									System.Collections.Generic;
								
									using
									System.ComponentModel.DataAnnotations;
								
									using
									System.Linq;
								
									using
									System.Text;
								
									using
									System.Text.Encodings.Web;
								
									using
									System.Threading.Tasks;
								
									using
									Microsoft.AspNetCore.Authentication;
								
									using
									Microsoft.AspNetCore.Authorization;
								
									using
									Microsoft.AspNetCore.Identity;
								
									using
									Microsoft.AspNetCore.Identity.UI.Services;
								
									using
									Microsoft.AspNetCore.Mvc;
								
									using
									Microsoft.AspNetCore.Mvc.RazorPages;
								
									using
									RoleEnabledWebAssembly.Server.Models;
								
									namespace
									RoleEnabledWebAssembly.Server.Areas.Identity.Pages
								
{
								
									[AllowAnonymous]
								
									public
									class
									RegisterModel : PageModel
								
									{
								
									// *************************************************
								
									// This is the user that will be automatically
									
								
									// made an Administrator
								
									// *************************************************
								
									const
									string
									ADMINISTRATOR_USERNAME = "Admin@email";
								
									const
									string
									ADMINISTRATION_ROLE = "Administrators";
								
									private
									readonly
									SignInManager<ApplicationUser> _signInManager;
								
									private
									readonly
									UserManager<ApplicationUser> _userManager;
								
									private
									readonly
									RoleManager<IdentityRole> _roleManager;
								
									public
									RegisterModel(
								
									SignInManager<ApplicationUser> signInManager,
								
									UserManager<ApplicationUser> userManager,
								
									RoleManager<IdentityRole> roleManager)
								
									{
								
									_userManager = userManager;
								
									_signInManager = signInManager;
								
									_roleManager = roleManager;
								
									}
								
									[BindProperty]
								
									public
									InputModel Input {
									get;
									set; }
								
									public
									string
									ReturnUrl {
									get;
									set; }
								
									public
									IList<AuthenticationScheme> ExternalLogins {
									get;
									set; }
								
									public
									class
									InputModel
								
									{
								
									[Required]
								
									[EmailAddress]
								
									[Display(Name = "Email")]
								
									public
									string
									Email {
									get;
									set; }
								
									[Required]
								
									[StringLength(100, ErrorMessage =
								
									"The {0} must be at least {2} and at max {1} characters long.",
								
									MinimumLength = 6)]
								
									[DataType(DataType.Password)]
								
									[Display(Name = "Password")]
								
									public
									string
									Password {
									get;
									set; }
								
									[DataType(DataType.Password)]
								
									[Display(Name = "Confirm password")]
								
									[Compare("Password", ErrorMessage =
								
									"The password and confirmation password do not match.")]
								
									public
									string
									ConfirmPassword {
									get;
									set; }
								
									}
								
									public
									async Task OnGetAsync(string
									returnUrl =
									null)
								
									{
								
									ReturnUrl = returnUrl;
								
									ExternalLogins =
								
									(await _signInManager.GetExternalAuthenticationSchemesAsync())
								
									.ToList();
								
									}
								
									public
									async Task<IActionResult> OnPostAsync(string
									returnUrl =
									null)
								
									{
								
									returnUrl = returnUrl ?? Url.Content("~/");
								
									ExternalLogins =
								
									(await _signInManager.GetExternalAuthenticationSchemesAsync())
								
									.ToList();
								
									if
									(ModelState.IsValid)
								
									{
								
									var user =
								
									new
									ApplicationUser {
								
									UserName = Input.Email,
								
									Email = Input.Email };
								
									var result =
								
									await _userManager.CreateAsync(user, Input.Password);
								
									if
									(result.Succeeded)
								
									{
								
									// Set confirm Email for user
								
									user.EmailConfirmed =
									true;
								
									await _userManager.UpdateAsync(user);
								
									// ensure there is a ADMINISTRATION_ROLE
								
									var RoleResult =
								
									await _roleManager.FindByNameAsync(ADMINISTRATION_ROLE);
								
									if
									(RoleResult ==
									null)
								
									{
								
									// Create ADMINISTRATION_ROLE Role
								
									await _roleManager.CreateAsync(
								
									new
									IdentityRole(ADMINISTRATION_ROLE));
								
									}
								
									if
									(user.UserName.ToLower() == ADMINISTRATOR_USERNAME.ToLower())
								
									{
								
									// Put admin in Administrator role
								
									await _userManager.AddToRoleAsync(user, ADMINISTRATION_ROLE);
								
									}
								
									// Log user in
								
									await _signInManager.SignInAsync(user, isPersistent:
									false);
								
									return
									LocalRedirect(returnUrl);
								
									}
								
									foreach
									(var error
									in
									result.Errors)
								
									{
								
									ModelState.AddModelError(string.Empty, error.Description);
								
									}
								
									}
								
									// If we got this far, something failed, redisplay form
								
									return
									Page();
								
									}
								
									}
								
}
								

							

Detect Users In Administrators Role

image

Open WeatherForecastController.cs and add:

								
[Authorize(Roles = "Administrators")]
								

							

image

This will only allow users in the Administrators role to call this controller.

image

Open the Index.razor page and change all the code to:

								
@page "/"
								
@using
									Microsoft.AspNetCore.Components.Authorization;
								
<AuthorizeView Roles="@ADMINISTRATION_ROLE">
								
									<p>You are an Administrator</p>
								
</AuthorizeView>
								
@code {
								
									[CascadingParameter]
								
									private
									Task<AuthenticationState> authenticationStateTask {
									get;
									set; }
								
									string
									ADMINISTRATION_ROLE = "Administrators";
								
}
								

							

This will display “You are an Administrator” only if the logged in user is in the Administrators role.

Test The Application

image

Hit F5 to run the application.

image

Click the button to Register a new user.

image

Create a user named Admin@Email .

image

If this is the first time you have created a user it will run migrations.

image

Complete the migrations by reloading the web browser.

image

You should be logged in (if not, click the Login link and log in as Admin@Email ).

The code on the Index.razor page will detect that the user is an Administrator and indicate that.

image

When we go to the Fetch data page, that calls the WeatherForecastController.cs controller, it will work.

If you go to that page without logging in, or as another user who is not an Administrator, it will throw an error.

image

If we go to the F12 Dev tools in our web browser and look at the userinfo call in the Network tab, we see the user is in the Administrators role.

Multiple Roles For a User

If we add the following code to add two roles for a user named helpdeskadmin@email:

								
									var HelpDeskRoleResult =
								
									await _roleManager.FindByNameAsync("Helpdesk");
								
									if
									(HelpDeskRoleResult ==
									null)
								
									{
								
									// Create "Helpdesk" Role
								
									await _roleManager.CreateAsync(
								
									new
									IdentityRole("Helpdesk"));
								
									}
								
									if
									(user.UserName.ToLower() == "helpdeskadmin@email")
								
									{
								
									// Put user in Administrator and Helpdesk roles
								
									await _userManager.AddToRoleAsync(user, ADMINISTRATION_ROLE);
								
									await _userManager.AddToRoleAsync(user, "Helpdesk");
								
									}
								

							

image

The Fetch data page, that validates the role membership server side, works…

image

But, the client side role validation code on the home page does not.

image

Even though the Network tools indicate that the user is properly in both roles.

image

The Admin@email account, that is only in one role, still works however.

image

Add a file called RolesClaimsPrincipalFactory.cs to the Client project with the following code:

								

								

								
									// From: https://github.com/dotnet/AspNetCore.Docs/issues/17649
								
									// By Javier Calvarro Nelson https://github.com/javiercn
									
								
									using
									Microsoft.AspNetCore.Components.WebAssembly.Authentication;
								
									using
									Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
								
									using
									System.Linq;
								
									using
									System.Security.Claims;
								
									using
									System.Text.Json;
								
									using
									System.Threading.Tasks;
								
									namespace
									RoleEnabledWebAssembly.Client
								
{
								
									public
									class
									RolesClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
								
									{
								
									public
									RolesClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) :
									base(accessor)
								
									{
								
									}
								
									public
									override
									async ValueTask<ClaimsPrincipal> CreateUserAsync(
								
									RemoteUserAccount account, RemoteAuthenticationUserOptions options)
								
									{
								
									var user = await
									base.CreateUserAsync(account, options);
								
									if
									(user.Identity.IsAuthenticated)
								
									{
								
									var identity = (ClaimsIdentity)user.Identity;
								
									var roleClaims = identity.FindAll(identity.RoleClaimType);
								
									if
									(roleClaims !=
									null
									&& roleClaims.Any())
								
									{
								
									foreach
									(var existingClaim
									in
									roleClaims)
								
									{
								
									identity.RemoveClaim(existingClaim);
								
									}
								
									var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
								
									if
									(rolesElem
									is
									JsonElement roles)
								
									{
								
									if
									(roles.ValueKind == JsonValueKind.Array)
								
									{
								
									foreach
									(var role
									in
									roles.EnumerateArray())
								
									{
								
									identity.AddClaim(new
									Claim(options.RoleClaim, role.GetString()));
								
									}
								
									}
								
									else
								
									{
								
									identity.AddClaim(new
									Claim(options.RoleClaim, roles.GetString()));
								
									}
								
									}
								
									}
								
									}
								
									return
									user;
								
									}
								
									}
								
}
								

							

image

Open the Program.cs file, in the Client project, and change:

								
									builder.Services.AddApiAuthorization();
								

							

to:

								
									// Call the factory
									
								
									builder.Services.AddApiAuthorization()
								
									.AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
								

								

								

							

image

Now it works properly.

Links

[Blazor][Wasm] Using roles with ApiAuthorization + Blazor

Secure an ASP.NET Core Blazor WebAssembly standalone app with the Authentication library

Azure AD Groups, Administrative Roles, and user-defined roles

Blazor WebAssembly 3.2.0 Release Candidate now available

An error has occurred. This application may no longer respond until reloaded. Reload 🗙