Securing Blazor WASM Client side only (no server) with AD Roles
HUBBER12 opened this issue · 3 comments
I'm having difficulty finding an article / directions to implement Authorization Roles for a Client side only Blazor WASM (no server). Many articles for Server+Client Blazor App, but none out there for Blazor Client side WASM only.
What I have working
- Azure Active Directory / Registered App with Custom User Roles
- Blazor WASM ( client only - no server ) - logging in and authenticating with AD
- Enterprise App registration in AD user+app role assigned
- @Attribute [Authorize] on the Blazor page successfully showing/hiding depending on authenticated or not.
What's not working
- @Attribute [Authorize Roles="MyRole1, MyRole2"]
When i try to add the roles to that @Attribute [Authorize] statement, it always rejects.
Cant find any examples/articles of what needs to be modified ,
for Client side Only Blazor WASM,
to allow the authorization to see/use the roles on the Blazor side.
So thought it would be a good article to create.
one more note - in case it helps
I was able to inject AuthenticationStateProvider on the Blazor Page
and use following code to verify the Role i want is getting from AD all way through Blazor side.
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var claims = user.Claims?.ToList();
// loop through and print shows the role
so it's getting form AD to Blazor... but the blazor page is not Authorizing per that role correctly
Just FYI - I got this to work by:
- reading "roles" claim string coming ack from AD after login
- splitting the "roles" string by comma into a list of claims
- cleaning each string of dirty characters [ ' and others etc..
- then adding each string (role) back as a separate claim AddClaim.Role
- and i also had to specify in the builder services to look for 'role' not 'roles'.
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("https://graph.microsoft.com/User.Read");
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
options.UserOptions.RoleClaim = "role"; // need this to specify claims 'role' instead of 'roles'
}).AddAccountClaimsPrincipalFactory<AccountClaimsPrincipalFactoryEx>();
notice the options.UserOptions.RoleClaim = "role"
that line makes it look for role and not roles
and then adding this class in my project did the rest
public class AccountClaimsPrincipalFactoryEx : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public AccountClaimsPrincipalFactoryEx(IAccessTokenProviderAccessor accessor) : base(accessor)
{
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
// if user is already authenticated return
if (!user.Identity.IsAuthenticated)
return user;
// make a list of the 'roles' AD provides
var rolesClaimString = ((ClaimsIdentity)user.Identity).FindFirst("roles").Value;
List<string> claimsListStrings = rolesClaimString.Split(',').ToList();
foreach(string roleString in claimsListStrings)
{
string cleanNameRoleString = Regex.Replace(roleString, "[^A-Za-z0-9]", ""); // Needed to clean string of nonalpha chars
if (String.IsNullOrWhiteSpace(cleanNameRoleString))
continue;
// add the role name to the current user
((ClaimsIdentity)user.Identity).AddClaim(new Claim("role", cleanNameRoleString));
}
return user;
}
}
After that these types of things then worked...
@attribute [Authorize(Roles = "Admin")]
...
<AuthorizeView Roles="RoleName1, SomeName2">
<Authorized>
<h3>Hooray, look I'm alive !!!</h3>
</Authorized>
<NotAuthorized>
<h3>Nope - not yet</h3>
</NotAuthorized>
</AuthorizeView>
Found a workaround noted in comments. not sure if there is a better, more standard way to get AD to Authorize by role, but works for now.