Let me introduce you to my web development tools - Blazor and Razor :)

Groups-based authorization in Blazor WebAssembly

This post was most recently updated on April 29th, 2022.

4 min read.

This article will take a look at a neat authorization option for Blazor WebAssembly – utilizing group memberships when defining policies. This isn’t a tutorial or an overview of Blazor – rather, we start from you already having your Blazor WebAssembly project set up, and we’ll be taking a look into modifying it to use group membership claims with policy-based authorization. This is a bit trickier than using roles, which Blazor WebAssembly already supports quite well.

But before that – Blazor? That sounds familiar – what was it again?

Background

Blazor is great. It’s all the rage. Really.

You can write slick code that runs in the browser without writing a line of JavaScript. Although, if you do like your JavaScripts, you can call them from C#. Or you can call C# from JavaScript. And it all (kind of) just works.

It just works" - T̶o̶d̶d̶ Godd Howard 2015 - YouTube

Anyway – with Blazor, you currently have 2 options for your implementation (with a couple of additional ones coming in): Blazor Server (a lot like ASP.NET MVC with Razor pages), and Blazor WebAssembly (pretty much the same but separated in a couple of different projects).

In reality, though, the former is hosted on a server as a SPA that requires constant connection and maintains a session, and the latter is compiled into WebAssembly that’s loosely coupled with a server backend but doesn’t require an always-on connection.

Ah, but this wasn’t supposed to be a Blazor 101 or an overview of Blazor architectures. No, no – we’re simply looking into the specific case of utilizing group memberships for your authorization in Blazor wasm. How’s that going to work, then?

Solution

It took me a couple of hours of tweaking, but here’s something that works:

Time needed: 30 minutes.

How to implement Policy-based authorization based on group claims in Blazor Webassembly?

  1. Configure your app registrations to return group membership claims

    This can be done by modifying your app’s manifest in Azure AD Portal – I have another article about that here: https://www.koskila.net/iterating-group-memberships-using-claims-in-net-core/

  2. Add a new AuthorizationRequirement

    This is a super simple class that makes it possible to define requirements for different policies. Don’t ask too many questions now, you’ll see how it works.

    Since you’re probably using Blazor WebAssembly (that’s why you’re here, right?), you might want to add this to the Shared project.

    We’ll call the class “GroupRequirement” here, but you can call it whatever you like.

    public class GroupRequirement : IAuthorizationRequirement
    {
    public string GroupGuid { get; }
    public GroupRequirement(string groupGuid) { GroupGuid = groupGuid; }
    }

    You’ll probably need to add a dependency on Microsoft.Authentication.WebAssembly.Msal to your Shared project.

  3. Add a new AuthorizationHandler

    This simple class checks whether the requirement defined above actually is fulfilled. Goes into the Shared -project as well.

    public class GroupRequirementHandler : AuthorizationHandler
    {
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
    GroupRequirement requirement)
    {
    // TODO: You'll probably also want to check for the correct issuer
    if (context.User.Claims.Any(x => x.Value == requirement.GroupGuid))
    {
    context.Succeed(requirement);
    }
    return Task.CompletedTask; } }
    }

  4. Add a custom user factory class

    This should look something like shown in Appendix 1 (for groups – you can also access roles or other claims if you need to!) It’ll go under the Shared -project as well.

    Now, if WordPress supported adding the piece of code right here, I would, but it doesn’t. No matter how it’s formatted, it’ll look like this:

    Sometimes I feel like WordPress is a bit of a pain… 🙃

  5. Add your policies to Server -project

    Note, that you need to also have services.AddAuthentication() somewhere – but for that, I have no sample, as it’s quite different between Azure AD and IdentityServer, for example.

    services.AddAuthorization(options =>
    {
    options.AddPolicy("MemberOfGroup", policy =>
    policy.Requirements.Add(new GroupRequirement("your-group-id")));
    });
    services.AddSingleton<IAuthorizationHandler, GroupRequirementHandler>();

  6. Add your policies to Client -project

    Blazor WebAssembly Client-project uses Program.cs for the startup, and you will need to set up your policies there, too.

    And the Authentication-stuff from above applies here as well.

    // Add Authorization, policies and their handlers
    builder.Services.AddAuthorizationCore(options =>
    {
    options.AddPolicy("MemberOfGroup", policy =>
    policy.Requirements.Add(new GroupRequirement("your-group-id")));
    });
    builder.Services.AddSingleton<IAuthorizationHandler, GroupRequirementHandler>();
    builder.Services.AddApiAuthorization().AddAccountClaimsPrincipalFactory<CustomUserFactory>();


Et voila! Now you can use it somewhat like this:

[Authorize(Policy = MemberOfGroup)]
[ApiController]
[Route("api/[controller]")]
public class YourController : Controller

And this:

<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>
        <AuthorizeView Policy="MemberOfGroup">
            <li class="nav-item px-3">
                <NavLink class="nav-link" href="secretPage">
                    <span class="oi oi-lightbulb" aria-hidden="true"></span> User Management
                </NavLink>
            </li>
        </AuthorizeView>
    </ul>
</div>

All good? Let me know if it worked for you!

References & notes

Appendix 1 – CustomUserFactory.cs

using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomUserFactory
    : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
    public CustomUserFactory(IAccessTokenProviderAccessor accessor)
        : base(accessor)
    {
    }

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

        if (user.Identity.IsAuthenticated)
        {
            var identity = (ClaimsIdentity)user.Identity;

            var groupClaims = identity.Claims.Where(x => x.Type == "groups").ToArray();
            var allClaims = identity.Claims.Where(x => x.Type.Contains("group")).ToList();

            if (groupClaims.Any())
            {
                foreach (var existingClaim in groupClaims)
                {
                    identity.RemoveClaim(existingClaim);
                }


                List<Claim> claims = new List<Claim>();

                foreach (var g in groupClaims)
                {
                    var groupGuids = JsonSerializer.Deserialize<string[]>(g.Value);

                    foreach (var claim in groupGuids)
                    {
                        claims.Add(new Claim(groupClaims.First().Type, claim));
                    }

                }

                foreach (var claim in claims)
                {
                    identity.AddClaim(claim);
                }
        }

        return user;
    }
}
mm
5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments