Blazor WebAssembly runs entirely in the browser. That single fact shapes everything about how you implement authorization, because nothing the client decides can be trusted. A user can open dev tools, edit memory, and flip any boolean you use to hide a button.
So role-based access control in a WASM app is really two separate jobs:
- Cosmetic — show users only the parts of the UI they're allowed to use, so the app feels coherent.
- Enforced — make sure the API rejects anything a user shouldn't be able to do, regardless of what the client sends.
This post covers both, driven from Azure AD app roles.
Step 1: Define app roles in Azure AD
In the Azure portal, open your app registration → App roles → create a role. The important field is the Value — that's the string that lands in the token. For example, a role with value BasicUser.
Then assign users to that role under Enterprise applications → your app → Users and groups. Azure AD will now include the role in the roles claim of the access token issued to that user.
Step 2: Map the role claim in the client
Blazor WASM doesn't automatically know that the roles claim should map to .NET role checks. You tell it during authentication setup:
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://your-api-id/access_as_user");
// Make the "roles" claim drive IsInRole / [Authorize(Roles = ...)]
options.UserOptions.RoleClaim = "roles";
});
With that mapping in place, the standard authorization primitives start working off your Azure AD roles.
Step 3: Conditional UI with AuthorizeView
For showing and hiding pieces of UI, AuthorizeView is the cleanest tool:
Run report
You don't have access to this feature.
This is the cosmetic layer. It's genuinely useful — it stops users from being confused by controls they can't use — but on its own it secures nothing.
Step 4: Restrict navigation
A common pattern is to hide whole sections of the nav menu. You can check roles imperatively by injecting the authentication state:
@inject AuthenticationStateProvider AuthState
@if (_isBasicUser)
{
Reports
}
@code {
private bool _isBasicUser;
protected override async Task OnInitializedAsync()
{
var state = await AuthState.GetAuthenticationStateAsync();
_isBasicUser = state.User.IsInRole("BasicUser");
}
}
You can also protect the routed pages themselves with an attribute, so that even a user who types the URL directly gets bounced to the "not authorized" view:
@page "/reports"
@attribute [Authorize(Roles = "BasicUser")]
Again — useful, but still client-side. A determined user can bypass all of it.
Step 5: The part that actually matters — enforce on the server
Every endpoint behind the UI must independently check the role. The browser-side checks are a convenience; the API is the boundary that counts.
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
[HttpPost("run")]
[Authorize(Roles = "BasicUser")]
public async Task RunReport()
{
// Only reachable by a token that actually carries the role.
// ...
return Ok();
}
}
Because the same Azure AD token carries the same roles claim to the API, the server validates the role from a source the client can't forge. If someone strips the client-side checks and calls the endpoint directly, the [Authorize] attribute rejects them.
The mental model to take away
Think of it as defense in two layers with very different jobs:
- The Blazor WASM layer makes the app pleasant and coherent — users see what's relevant to them.
- The API layer makes the app secure — it assumes the client is hostile and validates every role on its own.
If you only do the client side, you have a UI that looks locked down and an API that's wide open. If you only do the server side, you have a secure app with a confusing UI full of buttons that error out. You want both, and it's worth being explicit about which layer you're working on at any given moment — because they're easy to conflate, and conflating them is exactly how WASM apps end up insecure.
Top comments (0)