Up until now, we have learned how to use AuthenticationStateProvider in Blazor WebAssembly. Additionally, we have learned how to create registration functionality as well as Login and Logout functionalities in our application. But what about the roles? Well, in this article, we are going to learn how to create a role-based authorization in our Blazor WebAssembly application and how to modify AuthenticationStateProvider to support this feature.
For the complete navigation for this series, you can visit the Blazor Series page.
So, let’s start.
Adding Roles to the Database
If we inspect the Startup.cs
class in the Web API project, or the Program
class in .NET 6 and above, we are going to see that we already support roles:
services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<ProductContext>();
So, all we have to do is to add different roles to the database. To do that, let’s create a new RoleConfiguration
class inside the Configuration
folder and modify it:
public class RoleConfiguration : IEntityTypeConfiguration<IdentityRole> { public void Configure(EntityTypeBuilder<IdentityRole> builder) { builder.HasData( new IdentityRole { Name = "Viewer", NormalizedName = "VIEWER" }, new IdentityRole { Name = "Administrator", NormalizedName = "ADMINISTRATOR" } ); } }
Here we just create two roles that we are going to seed to the database.
After that, we are going to modify the OnModelCreating
method in the ProductContext
class:
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new ProductConfiguration()); modelBuilder.ApplyConfiguration(new RoleConfiguration()); }
With this in place, we are ready to create our migration files:
PM> Add-Migration InitialRoleSeed
And update the database:
PM> Update-Database
With that, we have our roles added to the database.
Just one more thing. Since we already have a single user in our database, we can assign an administrator role to that user. All we have to do is to write a simple INSERT query in the SQL Management Studio:
INSERT INTO AspNetUserRoles VALUES ('22da8bd7-7ba3-40ad-9ebd-f0c149e759d6','c2671493-8bcf-4a82-969b-6c5fbc2cd5e0')
Of course, you have to replace these id values with your user and role id values.
Supporting Role-Based Authorization with Register Action and Claims
What we want to do here is to assign a Viewer role to every user registered through the Registration form. To do that, we have to slightly modify the RegisterUser
action in the Accounts
controller:
[HttpPost("Registration")] public async Task<IActionResult> RegisterUser([FromBody] UserForRegistrationDto userForRegistration) { if (userForRegistration == null || !ModelState.IsValid) return BadRequest(); var user = new IdentityUser { UserName = userForRegistration.Email, Email = userForRegistration.Email }; var result = await _userManager.CreateAsync(user, userForRegistration.Password); if (!result.Succeeded) { var errors = result.Errors.Select(e => e.Description); return BadRequest(new RegistrationResponseDto { Errors = errors }); } await _userManager.AddToRoleAsync(user, "Viewer"); return StatusCode(201); }
That’s all it takes, but we require one more thing. We want to add this role or multiple roles related to a single user, inside the JWT claims. To do that, we have to modify the private GetClaims
method in the same controller:
private async Task<List<Claim>> GetClaims(IdentityUser user) { var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Email) }; var roles = await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } return claims; }
Additionally, since this method is now the async one, we have to add the await
keyword while calling it inside the Login
action:
var claims = await GetClaims(user);
Excellent.
Once we send the Postman request to the Login
action, we are going to get the valid token as we did in a previous article. But this time, if we inspect the token, we are going to find an additional role claim inside it:
Nicely done.
We can move on to the Blazor WebAssembly part.
Role-Based Authorization with the Blazor Client Application
In a previous part, we have implemented our AuthenticationStateProvider
with the JwtParser
class that extracts claims from our token. But in that class, we didn’t cover the role claims. So, it is time to change that.
Let’s first modify the ParseClaimsFromJwt
method:
public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); ExtractRolesFromJWT(claims, keyValuePairs); claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); return claims; }
As you can see, we are calling an additional method to extract roles from JWT. That said, let’s create this missing method:
private static void ExtractRolesFromJWT(List<Claim> claims, Dictionary<string, object> keyValuePairs) { keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles); if (roles != null) { var parsedRoles = roles.ToString().Trim().TrimStart('[').TrimEnd(']').Split(','); if (parsedRoles.Length > 1) { foreach (var parsedRole in parsedRoles) { claims.Add(new Claim(ClaimTypes.Role, parsedRole.Trim('"'))); } } else { claims.Add(new Claim(ClaimTypes.Role, parsedRoles[0])); } keyValuePairs.Remove(ClaimTypes.Role); } }
Here, we try to extract roles from the keyValuePairs
dictionary. If the roles exist, we split the roles into a string array. If we have more than one role, we iterate through each of them and add them to the claims list. Otherwise, we just add that single role to the claims list. Also, we are using the Trim
, TrimStart
, and TrimEnd
methods to remove the square brackets and quotation marks from our roles. If a user has multiple roles, the roles object looks like this: ["FirstRole","SecondRole"]
. So, by using all the Trim
methods, we are removing these brackets and quotation marks.
Now, we want to modify the NotifyUserAuthentication
method from the AuthStateProvider
class:
public void NotifyUserAuthentication(string token) { var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")); var authState = Task.FromResult(new AuthenticationState(authenticatedUser)); NotifyAuthenticationStateChanged(authState); }
As you can see, we don’t accept just an email as a parameter but the entire token. Additionally, for the ClaimsIdentity
, we use all the claims parsed from the JwtParser
class.
Due to this change, we have to modify the call to this method from the Login
method inside the AuthenticationService
class:
public async Task<AuthResponseDto> Login(UserForAuthenticationDto userForAuthentication) { //previous code await _localStorage.SetItemAsync("authToken", result.Token); ((AuthStateProvider)_authStateProvider).NotifyUserAuthentication(result.Token); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token); return new AuthResponseDto { IsAuthSuccessful = true }; }
We are using the entire token as a parameter instead of just an email.
Testing Role-Based Authorization
Before we start with testing, we are going to register another user in our application. After registration, this user will have the Viewer role.
Right now, only authorized users can access the Products
page. But, let’s change that a bit. Let’s assume that only administrators can access this page. To do that, we have to modify the [Authorize]
attribute in the Products.razor
file:
@attribute [Authorize(Roles = "Administrator")]
We add the Roles
property with the Administrator
value.
Now, if we start both applications and login as a viewer, we won’t be able to access this page:
But if we try to login with the administrator account:
We can see that we can access the page.
Additionally, if we don’t want to allow users with the Viewer role to even see the Products
menu item, we can do that by modifying the NavMenu.razor
file:
<AuthorizeView Roles="Administrator"> <Authorized> <li class="nav-item px-3"> <NavLink class="nav-link" href="products"> <span class="oi oi-list-rich" aria-hidden="true"></span> Products </NavLink> </li> </Authorized> </AuthorizeView>
With the previous logic, the Products menu was visible only to authorized users. Now, we allow this link to be visible only to authorized users in the Administrator role.
Again, you can log in with both users and confirm that the Viewers can’t see the Products menu item.
Finally, since the client code can be bypassed, we should ensure that our API’s endpoint is protected properly as well:
[Route("api/products")] [ApiController] [Authorize(Roles = "Administrator")] public class ProductsController : ControllerBase
And that’s all it takes.
We have implemented Role-Based authorization in our Blazor WebAssembly application.
Conclusion
In this article, we have learned how to implement Role-Based authorization with Blazor WebAssembly and our API application. We have seen how to add roles to the database, how to include them in the JWT as claims, and how to parse them on the client level.
Additionally, we have learned how to protect endpoints, limit access to some pages, and how to hide pages using roles in our Blazor WASM application.
In the next article, we are going to show you how to refresh your access token with Blazor WebAssembly and ASP.NET Core Web API.
Best regards.