Search code examples
blazorasp.net-core-3.1oktablazor-webassembly

Unable to map Okta Groups Claim to Roles Claim in Blazor WASM Client


Issue

I am trying to leverage the AuthorizeView component in Blazor to hide/show different parts of the page based on a users role. I am using OIDC connected to OKTA as the auth provider.

By default, OKTA return the Roles scope as a Groups claim within the id_token. I have attempted to force authentication provider to look at the groups claim for the roles as seen in the code below.

My test account has the appropriate permissions, as I can see it within the Groups claim. I am unable to get this mapping to work.

Has anyone had any similar issues and/or found a solution to this?

Sample Code

-- Program.cs --
public static async Task Main(string[] args)
{
    ...

    builder.Services.AddOidcAuthentication(options =>
    {
        options.ProviderOptions.Authority = "***";
        options.ProviderOptions.ClientId = "***";
        options.ProviderOptions.DefaultScopes.Add("roles");
        options.ProviderOptions.ResponseType = "token id_token";
        
        options.UserOptions.RoleClaim = "groups";
        options.UserOptions.NameClaim = "name";
    });

    ....
}

-- MyPage.razor --
<AuthorizeView Roles="Admin">
    <Authorized>
        Authorized
    </Authorized>
    <NotAuthorized>
        Not Authorized
    </NotAuthorized>
</AuthorizeView>

Solution

  • Solution

    I found the following article: http://blazorhelpwebsite.com/Blog/tabid/61/EntryId/4376/Implementing-Roles-In-Blazor-WebAssembly.aspx, which explains how to use a custom Claims Principal Factory.

    I copied the code from the article and adjusted accordingly to my needs

    RolesClaimsPrincipalFactory.cs

    public class RolesClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
    {
        public RolesClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
        {
        }
    
        public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
            RemoteUserAccount account, RemoteAuthenticationUserOptions options)
        {
            var user = await base.CreateUserAsync(account, options);
            if (!user.Identity.IsAuthenticated)
            {
                return user;
            }
    
            var identity = (ClaimsIdentity) user.Identity;
            var roleClaims = identity.FindAll(claim => claim.Type == "groups");
            if (roleClaims == null || !roleClaims.Any())
            {
                return user;
            }
    
            foreach (var existingClaim in roleClaims)
            {
                identity.RemoveClaim(existingClaim);
            }
    
            var rolesElem = account.AdditionalProperties["groups"];
            if (!(rolesElem is JsonElement roles))
            {
                return user;
            }
    
            if (roles.ValueKind == JsonValueKind.Array)
            {
                foreach (var role in roles.EnumerateArray())
                {
                    identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
                }
            }
            else
            {
                identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
            }
    
            return user;
        }
    }
    

    Program.cs

    public class Program
    {
        public static async Task Main(string[] args)
        {
            ...
    
            builder.Services.AddOidcAuthentication(options =>
            {
                options.ProviderOptions.Authority = ******;
                options.ProviderOptions.ClientId = ******;
                options.ProviderOptions.DefaultScopes.Add("roles");
                options.ProviderOptions.ResponseType = "token id_token";
    
                options.UserOptions.RoleClaim = "role";
            }).AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
    
            ...
        }
    }
    

    Key Takeaways

    1. You are required to specify a RoleClaim options.UserOptions.RoleClaim = "role";. If you do not you will get a NullReferenceException.
    2. Implementing the custom claims principal is done by using the extension method AddAccountClaimsPrincipalFactory<T>().
    3. This solution seems like a niche case for OKTA Auth, Blazor WASM and OIDC.