Search code examples
.netoauthblazorokta.net-5

Mapping Okta Claims to Roles in .NET Blazor


I have a .NET 5.0 (upgraded from .NET Core) hosted Blazor solution integrated with Okta.

I am able to log in without issue, but when I am redirected back to my app I'm running into an issue mapping roles to claims.


   Unhandled exception rendering component: InvalidOperation_EnumFailedVersion

System.InvalidOperationException: InvalidOperation_EnumFailedVersion

  at System.Collections.Generic.List`1.Enumerator[[System.Security.Claims.Claim, System.Security.Claims, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]].MoveNextRare()

  at System.Collections.Generic.List`1.Enumerator[[System.Security.Claims.Claim, System.Security.Claims, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]].MoveNext()

  at System.Security.Claims.ClaimsIdentity.FindAll(Predicate`1 match)+MoveNext()

  at WFBC.Client.RolesClaimsPrincipalFactory.CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options)

  at Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService`3.<GetAuthenticatedUser>d__26[[Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationState, Microsoft.AspNetCore.Components.WebAssembly.Authentication, Version=5.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteUserAccount, Microsoft.AspNetCore.Components.WebAssembly.Authentication, Version=5.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[Microsoft.AspNetCore.Components.WebAssembly.Authentication.OidcProviderOptions, Microsoft.AspNetCore.Components.WebAssembly.Authentication, Version=5.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].MoveNext()

The error is being thrown here:

namespace WFBC.Client
{
    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;
        }
    }
}

This foreach loop seems to be the source of the exception:

            foreach (var existingClaim in roleClaims)
            {
                identity.RemoveClaim(existingClaim);
            }

The only info I've been able to find seems to point to attempting to modify an IEnumerable, but in this case I am not. roleClaims is an IEnumerable, but identity is a ClaimsIdentity object.

This was working previously, so I'm not sure if it's caused by the .NET 5.0 upgrade or some changes to the Okta nuget packages. Unfortunately Blazor debugging into the client project is far from production ready, and not working for me currently.


Solution

  • Here is what ended up working for me:

    namespace WFBC.Client
    {
        public class RolesClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
        {
            public RolesClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor) { }
    
            public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
             RemoteUserAccount account,
             RemoteAuthenticationUserOptions options)
            {
                ClaimsPrincipal user = await base.CreateUserAsync(account, options);
    
                if (user.Identity.IsAuthenticated)
                {
                    var identity = (ClaimsIdentity)user.Identity;
                    Claim[] roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();
                    var userClaims = user.Claims;
    
                    if (roleClaims != null && roleClaims.Any())
                    {
                        foreach (Claim existingClaim in roleClaims)
                        {
                            identity.RemoveClaim(existingClaim);
                        }
                    }
                    try
                    {
                        if (userClaims != null && userClaims.Any())
                        {
                            foreach (Claim userClaim in userClaims)
                            {
                                if (userClaim.Type == "groups" && userClaim.Value != null)
                                {
                                    string groups = userClaim.Value;
                                    if (!string.IsNullOrEmpty(groups))
                                    {
                                        string[] userGroups = JsonSerializer.Deserialize<string[]>(userClaim.Value);
                                        foreach (string userGroup in userGroups)
                                        {
                                            identity.AddClaim(claim: new Claim(ClaimTypes.Role.ToString(), userGroup));
                                        }
                                    }
                                }
                            }
                        }
                    }
                    catch
                    {
    
                    }
    
                }
                return user;
            }
        }
    }
    

    Additionally, I found that IsInRole() always returns false. Consequently, RequireRole() would not work. There are a number of discussions on this, and the issue may be that it expects a specific object type. I found it easiest to just use RequireClaim() in my policies instead:

    RequireClaim(ClaimTypes.Role.ToString(), "Managers")