5/16/2020 Admin
Implementing Roles In Blazor WebAssembly
You can implement roles in a Blazor WebAssembly application.
Install Blazor WebAssembly
Blazor has two modes, Blazor Server and Blazor WebAssembly.
For this tutorial, install the latest Blazor WebAssembly version.
Create The Application
Open Visual Studio.
Create a New Project.
Create a Blazor App.
Give the project the name RoleEnabledWebAssembly and click Create.
Select the Blazor WebAssembly template, select ASP.NET Core hosted and click the change link under Authentication.
Select Individual User Accounts and click OK.
Click Create.
The application will be created.
The application will display in Visual Studio.
Enable Role Support
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");
Blazor dynamically creates a Register page, but, we need to override it so that we can add code that will:
- Create an Administrators role (if one does not already exist)
- 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
Open WeatherForecastController.cs and add:
[Authorize(Roles = "Administrators")]
This will only allow users in the Administrators role to call this controller.
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
Hit F5 to run the application.
Click the button to Register a new user.
Create a user named Admin@Email.
If this is the first time you have created a user it will run migrations.
Complete the migrations by reloading the web browser.
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.
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.
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");
}
The Fetch data page, that validates the role membership server side, works…
But, the client side role validation code on the home page does not.
Even though the Network tools indicate that the user is properly in both roles.
The Admin@email account, that is only in one role, still works however.
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;
}}}
Open the Program.cs file, in the Client project, and change:
builder.Services.AddApiAuthorization();
to:
// Call the factory
builder.Services.AddApiAuthorization().AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
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