JeremyLikness/jeremylikness-blog

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

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.