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.
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?
- 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/
- 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. - 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; } }
} - 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… 🙃 - 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>(); - 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
- This is a pretty good tutorial on configuring CustomUserFactory.cs for roles:
- https://stackoverflow.com/questions/64968789/blazor-cascading-authorizeview-policy-not-working
- https://docs.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-5.0&tabs=visual-studio&viewFallbackFrom=aspnetcore-3.0#authorizeview-component
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;
}
}
- “Performing cleanup” – Excel is stuck with an old, conflicted file and will never recover. - November 12, 2024
- How to add multiple app URIs for your Entra app registration? - November 5, 2024
- How to access Environment Secrets with GitHub Actions? - October 29, 2024