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.

To download the source code for this article, you can visit the Role-Based Authorization with Blazor WASM repository.

For the complete navigation for this series, you can visit the Blazor Series page.

We are going to divide this article into the following topics:

So, let’s start.

Adding Roles to the Database

If we inspect the Startup.cs class in the Web API project, 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:

Additional Role Claim in the JWT for the Role-Based Authorization

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:

Access protected page with role without enough rights - Role-Based Authorization

But if we try to login with the administrator account:

Role-Based authorization with admin 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 the 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.