In this article, we are going to learn more about the Blazor WebAssembly Role-Based Security and how IdentityServer4 fits in the whole story. Also, we are going to include our API application in this process and protect our endpoints with the required role.

To download the source code for this article, you can visit our Blazor WebAssembly Role-Based Security repository.

It is very important that you are familiar with the IdentityServer4, OAuth2, and OIDC concepts. If you are not, we strongly suggest you reading our IdentityServer4, OAuth2, and OIDC series. There, you can find complete navigation for this series as well.

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

So, let’s start.

Showing Claims in the Blazor WebAssembly Application

Before we start with the Blazor WebAssembly role-based security implementation, we are going to list all the claims from the authenticated user. To do that, let’s create a new Privacy component under the Pages folder:

@page "/privacy"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

<h3>Privacy</h3>

<AuthorizeView>
    <h2>
        Hello @context.User.Identity.Name,
        here's the list of your claims:
    </h2>
    <ul>
        @foreach (var claim in context.User.Claims)
        {
            <li><b>@claim.Type</b>: @claim.Value</li>
        }
    </ul>
</AuthorizeView>

The route for this page is /privacy and it is protected with the [Authorize] attribute against unauthorized users. Inside the AuthorizeView component, we use the context object to show the name of the user and to iterate through all the claims.

If you are not familiar with the AuthorizeView component or just want to learn more about all these components Blazor provides for us, you can visit our AuthenticationStateProvider article.

After the Privacy page creation, we can modify the NavMenu.razor component and add a new link:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="privacy">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Privacy
            </NavLink>
        </li>
    </ul>
</div>

Now, we can start all three applications, log in with Mick’s credentials, and navigate to the Privacy page:

List of claims for the authenticated user in the Blazor WebAssembly application with IdentityServer4

As we can see, here are all the claims but, we can’t see roles.

Let’s deal with it.

IdentityServer4 Roles Configuration

Since we are using an already prepared IS4 project for this series, the roles are included and supported. If we open the InMemoryConfig class and inspect the GetIdentityResources method, we are going to find support for the roles:

public static IEnumerable<IdentityResource> GetIdentityResources() =>
    new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Address(),
        new IdentityResource("roles", "User role(s)", new List<string> { "role" }),
        new IdentityResource("position", "Your position", new List<string> { "position" }),
        new IdentityResource("country", "Your country", new List<string> { "country" })
    };

Here, we support the identity resource with the name roles, display name, and the role claim.

Also, if we take a look at our users in the same class, we are going to find roles attached to both of them:

public static List<TestUser> GetUsers() =>
    new List<TestUser>
    {
        new TestUser
        {
            ...
            Claims = new List<Claim>
            {
                ...
                new Claim("role", "Admin"),
                new Claim("position", "Administrator"),
                new Claim("country", "USA")
            }
        },
        new TestUser
        {
            ...
            Claims = new List<Claim>
            {
                ...
                new Claim("role", "Visitor"),
                new Claim("position", "Viewer"),
                new Claim("country", "USA")
            }
        }
    };

But, as we saw, this is not enough. We have to provide a way to send our role and other claims to the client application.

Creating CustomProfileService to Send Claims to the Client App

Let’s start by creating a new CustomProfileService class at the root of the IS4 application.

After the creation, we can modify it:

public class CustomProfileService : IProfileService
{
    public Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var sub = context.Subject.GetSubjectId();
        var user = InMemoryConfig.GetUsers()
            .Find(u => u.SubjectId.Equals(sub));

        context.IssuedClaims.AddRange(user.Claims);
        return Task.CompletedTask;
    }

    public Task IsActiveAsync(IsActiveContext context)
    {
        var sub = context.Subject.GetSubjectId();
        var user = InMemoryConfig.GetUsers()
            .Find(u => u.SubjectId.Equals(sub));

        context.IsActive = user != null;
        return Task.CompletedTask;
    }
}

The CustomProfileService class must inherit from the IProfileService interface and implement both GetProfileDataAsync and IsActiveAsync methods. In the first method, we include the required claims for a user using the context object. There, we fetch the SubjectId and use it to find a user from our InMemoryConfig class. After that, we just add all the claims to the IssuedClaims list and complete the Task.

In the second method, we determine if a user is currently allowed to obtain tokens. So, if we find a user based on the SubjectId, we return true, otherwise false.

We have one more thing to do.

Let’s register this new class as a profile service for the IdentityServer4 application:

services.AddIdentityServer()
    .AddTestUsers(InMemoryConfig.GetUsers())
    .AddDeveloperSigningCredential() //not something we want to use in a production environment;
    .AddProfileService<CustomProfileService>()
    .AddConfigurationStore(opt =>
        ...

Excellent.

Now, let’s start again all applications, successfully authenticate, and visit the Privacy page:

Role claim added to the claims list to support Blazor WebAssembly Role-Based Security actions

And there we go. The role claim is here alongside with other user’s claims.

Blazor WebAssembly Role-Based Security Implementation

It’s the right time to use this role claim to protect our Privacy page. To accomplish that, all we have to do is to slightly modify the Privacy page:

@page "/privacy"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Admin")]

<h3>Privacy</h3>

<AuthorizeView>
    <h2>
        Hello @context.User.Identity.Name,
        here's the list of your claims:
    </h2>
    <ul>
        @foreach (var claim in context.User.Claims)
        {
            <li><b>@claim.Type</b>: @claim.Value</li>
        }
    </ul>
</AuthorizeView>

With this change, we allow only authorized users with the Admin role to access this page.

Now, if we use Mick’s credentials to authenticate (since Mick has the Admin role), and try to navigate to the Privacy page, we are going to see a message that we are not authorized to access this resource.

That’s because the Blazor app is not using the role claim for the authorization actions. At least, not yet. To change that, we have to modify the Program.cs class:

builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("oidc", options.ProviderOptions);
    options.UserOptions.RoleClaim = "role";
});

Now, if we log in again as Mick, we will be able to access the Privacy page. Also, if we do the same with Jane’s credentials, we are not going to be able to visit this page.

Protecting API Endpoint

Our Privacy page is not communicating with any endpoint on the API’s side, but let’s change that.

First, let’s add a new Privacy.razor.cs file and modify it:

public partial class Privacy
{
    [Inject]
    public HttpClient HttpClient { get; set; }

    private List<string> _claims = new List<string>();

    protected override async Task OnInitializedAsync()
    {
        _claims = await HttpClient.GetFromJsonAsync<List<string>>("companies/privacy");
    }
}

We use our registered HttpClient from the previous article, and send a request to the Web API’s endpoint, which returns the claims from the access token.

Then, let’s modify the razor file, just to show these claims as well:

@page "/privacy"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Admin")]

<h3>Privacy</h3>

<AuthorizeView>
    <h2>
        Hello @context.User.Identity.Name,
        here's the list of your claims:
    </h2>
    <ul>
        @foreach (var claim in context.User.Claims)
        {
            <li><b>@claim.Type</b>: @claim.Value</li>
        }
    </ul>
</AuthorizeView>

<h3>Claims from the API</h3>
<ul>
    @foreach (var claim in _claims)
    {
        <li>@claim</li>
    }
</ul>

Finally, we have to add a new endpoint to the CompaniesController in the Web API project:

[HttpGet("Privacy")]
[Authorize(Roles = "Admin")]
public IEnumerable<string> Privacy()
{
    var claims = User.Claims.Select(c => $"{c.Type}: {c.Value}").ToList();

    return claims;
}

As you can see the [Authorize] attribute has the Roles property, where we state only users with the Admin role can access the resources from this endpoint.

Now, we can start all three apps, log in as Mick, and navigate to the Privacy page:

Claims from the API after protecting the API endpoint with the Role

We can see these (and many other) claims right below the claims from the client application.

So, this means our API’s endpoint is also protected with a Role-Based authorization.

Blazor WebAssembly Role-Based Security with Multiple Roles

We have to mention this specific case because it brings an additional level of complexity to our code. Let us show you what we mean.

Let’s first add one more role to Mick in the InMemoryConfig class on the IDP level:

new TestUser
{
      SubjectId = "a9ea0f25-b964-409f-bcce-c923266249b4",
      Username = "Mick",
      Password = "MickPassword",
      Claims = new List<Claim>
      {
          new Claim(JwtClaimTypes.Name, "Mick Mining"),
          new Claim("given_name", "Mick"),
          new Claim("family_name", "Mining"),
          new Claim("address", "Sunny Street 4"),
          new Claim("role", "Admin"),
          new Claim("role", "Manager"),
          new Claim("position", "Administrator"),
          new Claim("country", "USA")
      }
},

Now, let’s just remove the CompanyEmployeeOAuth database, so that as soon as we start our IDP app, it creates a new database with all the data from this class.

After we do that, we can start all the apps, log in as Mick, and inspect the Privacy page:

Blazor WebAssembly Role-Based Security with Multiple Roles - not working

We see the problem. We are logged in, but we don’t have an access to the Privacy page.

For now, let’s just comment out the [Authorize] attribute on the Privacy page and do the same steps again:

Multiple Roles displayed as array of strings

As we can see, we have the access to the API’s resources, which means the Role-Based authorization works. So, the problem is on the Blazor’s side with the way the multiple roles are displayed. Clearly, we have to split every single role into its own role claim.

First, let’s uncomment the [Authorize] attribute on the API level.

Then, let’s create a new ClaimsPrincipalFactory folder and a new MultipleRoleClaimsPrincipalFactory class under it:

public class MultipleRoleClaimsPrincipalFactory<TAccount> : AccountClaimsPrincipalFactory<TAccount> 
    where TAccount : RemoteUserAccount
{
    public MultipleRoleClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) 
        : base(accessor)
    {
    }

    public async override ValueTask<ClaimsPrincipal> CreateUserAsync(TAccount account, RemoteAuthenticationUserOptions options)
    {
        var user = await base.CreateUserAsync(account, options);

        var claimsIdentity = (ClaimsIdentity)user.Identity;

        if (account != null)
        {
            MapArrayClaimsToMultipleSeparateClaims(account, claimsIdentity);
        }

        return user;
    }

    private static void MapArrayClaimsToMultipleSeparateClaims(TAccount account, ClaimsIdentity claimsIdentity)
    {
        foreach (var prop in account.AdditionalProperties)
        {
            var key = prop.Key;
            var value = prop.Value;
            if (value != null &&
                (value is JsonElement element && element.ValueKind == JsonValueKind.Array))
            {
                claimsIdentity.RemoveClaim(claimsIdentity.FindFirst(prop.Key));

                var claims = element.EnumerateArray()
                    .Select(x => new Claim(prop.Key, x.ToString()));

                claimsIdentity.AddClaims(claims);
            }
        }
    }
}

Our class inherits from the AccountClaimsPrincipalFactory class and overrides the CreateUserAsync method. The AccountClaimsPrincipalFactory class provides a default implementation for converting a RemoteUserAccount into a ClaimsPrincipal. By overriding the CreateUserAsync method, we extract the ClaimsIdentity from the created user and call the MapArrayClaimsToMultipleSeparateClaims method. This method does exactly what the name states – it maps claims from an array to the multiply single claims.

We have one more thing to do. Let’s open the Program.cs class and replace the existing AccountClaimsPrincipalFactory with our custom one:

builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("oidc", options.ProviderOptions);
    options.UserOptions.RoleClaim = "role";
})
.AddAccountClaimsPrincipalFactory<MultipleRoleClaimsPrincipalFactory<RemoteUserAccount>>();

Good.

Now, let’s start the client app, log in, and inspect the Privacy page:

Blazor WebAssembly Role-Based Security with Multiple Roles - working

We can see the role claim split into multiple claims, and that our role-based authorization works with multiple roles for a single user.

Hiding UI Elements in Our Blazor WebAssembly Application

At this point, if we log in as Jane, we won’t be able to see the content of the Privacy page, but we still see the link in the menu bar. If we want to hide it, we can modify the NavMenu.razror file:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
        <AuthorizeView Roles="Admin">
            <Authorized>
                <li class="nav-item px-3">
                    <NavLink class="nav-link" href="privacy">
                        <span class="oi oi-list-rich" aria-hidden="true"></span> Privacy
                    </NavLink>
                </li>
            </Authorized>
        </AuthorizeView>
    </ul>
</div>

Now, if we log in as Mick, we will be able to see this menu. But if we do the same as Jane, the Privacy link will disappear.

Conclusion

So, we have learned a lot of things:

  • How to provide claims from IS4 to the Blazor WebAssembly application,
  • The way to use the role claim to improve the authorization process
  • How to handle multiple roles for a single user
  • The way to use roles to hide UI elements in our client application

In the next article, we are going to learn about Attribute-Based Access Control in Blazor WebAssembly application.

See you there.