Blazor cascading AuthorizeView Policy not working
Asked Answered
K

3

6

I'm working on a new project that will have some in depth policies for what user can and can't access/see, with Identity Server 4.

I'm trying to use AuthorizeView with policies to hide options in my navigation, but the views are cascading, meaning I have something like this:

<MatNavMenu>
<MatNavItem Href="/home" Title="Home"><MatIcon Icon="@MatIconNames.Home"></MatIcon>&nbsp; Home</MatNavItem>
<MatNavItem Href="/claims" Title="Claims"><MatIcon Icon="@MatIconNames.Vpn_key"></MatIcon>&nbsp; Claims</MatNavItem>
<AuthorizeView Policy="@PolicyNames.IdentitySystemAccess">
    <Authorized>
        <AuthorizeView Policy="@PolicyNames.AccessManagement">
            <Authorized>
                <MatNavSubMenu @bind-Expanded="@_accessSubMenuState">
                    <MatNavSubMenuHeader>
                        <MatNavItem AllowSelection="false">&nbsp; Access Management</MatNavItem>
                    </MatNavSubMenuHeader>
                    <MatNavSubMenuList>
                        <AuthorizeView Policy="@PolicyNames.User">
                            <Authorized>
                                <MatNavItem Href="users" Title="users"><MatIcon Icon="@MatIconNames.People"></MatIcon>&nbsp; Users</MatNavItem>
                            </Authorized>                               
                        </AuthorizeView>
                        <AuthorizeView Policy="@PolicyNames.Role">
                            <Authorized>
                                <MatNavItem Href="roles" Title="roles"><MatIcon Icon="@MatIconNames.Group"></MatIcon>&nbsp; Roles</MatNavItem>
                            </Authorized>
                        </AuthorizeView>
                    </MatNavSubMenuList>
                </MatNavSubMenu>
            </Authorized>
        </AuthorizeView>
    </Authorized>
</AuthorizeView>

I have checked that the claims required to fulfil the defined policies are present after the user is logged in, but for some reason the AuthorizeView isn't working.

I have updated my App.Razor to use AuthorizeRouteView. Any ideas as to why this is happening?

Note: I am using claims that are assigned to a role, but these are dynamic and I cannot use policy.RequireRole("my-role") in my policies, thus is use:

options.AddPolicy(PolicyNames.User, b =>
                {
                    b.RequireAuthenticatedUser();
                    b.RequireClaim(CustomClaimTypes.User, "c", "r", "u", "d");
                });

When my app runs, none of the items in the menu show up except for the home and claims items which are not protected by an AuthorizeView.

Kirstinkirstyn answered 23/11, 2020 at 12:52 Comment(7)
I have the same problem.. Did you solved ?Leathaleather
Hey Marshall, do you claims also look like mine in the example, where one claim has multiple values?Kirstinkirstyn
Yes! I've tried it in many ways! A ClaimType with multiple values, an extension verifying multiple claims. I'm working with several layouts, I don't know if this would be the problem ... On the home page I was able to verify a role-based policy. I just can't do it with Claims.Leathaleather
Have you tried the solution I just provided? And also I'm presuming you are using policies.Kirstinkirstyn
Hey Steve, I saw your solution now. If I understand correctly, the problem is that the Blazor client cannot decipher the claims in an array and would I have to separate them? I am not using IS4, I am using Microsoft Identity with standard jwt. I will try to make this improvement in the assembly of the claims on the client.Leathaleather
Yes that's correct, I think this solution works regardless of weather you use IS4 or not, the reference required for the ClaimsPrincipalFactory are all Microsoft and not unique to IS4. I added the name spaces to my exampleKirstinkirstyn
Man, you are AWESOME! Thank you soo much!!! Works perfect... I make something more easier... i will post my solution too to help others... Happy holidays!Leathaleather
K
4

The issue was due to the current lack of support for Blazor to read claims the are sent as arrays.

e.g. user: ["c","r","u","d"]

Can't be read.

To rectify this you need to add ClaimsPrincipalFactory.

e.g.

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;

namespace YourNameSpace
{
    public class ArrayClaimsPrincipalFactory<TAccount> : AccountClaimsPrincipalFactory<TAccount> where TAccount : RemoteUserAccount
    {
        public ArrayClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor)
        : base(accessor)
        { }


        // when a user belongs to multiple roles, IS4 returns a single claim with a serialised array of values
        // this class improves the original factory by deserializing the claims in the correct way
        public async override ValueTask<ClaimsPrincipal> CreateUserAsync(TAccount account, RemoteAuthenticationUserOptions options)
        {
            var user = await base.CreateUserAsync(account, options);

            var claimsIdentity = (ClaimsIdentity)user.Identity;

            if (account != null)
            {
                foreach (var kvp in account.AdditionalProperties)
                {
                    var name = kvp.Key;
                    var value = kvp.Value;
                    if (value != null &&
                        (value is JsonElement element && element.ValueKind == JsonValueKind.Array))
                    {
                        claimsIdentity.RemoveClaim(claimsIdentity.FindFirst(kvp.Key));

                        var claims = element.EnumerateArray()
                            .Select(x => new Claim(kvp.Key, x.ToString()));

                        claimsIdentity.AddClaims(claims);
                    }
                }
            }

            return user;
        }
    }
}

Then register this in your program/startup(depending on if you use .core hosted or not)like so:

builder.Services.AddOidcAuthentication(options =>
        {
            builder.Configuration.Bind("oidc", options.ProviderOptions);
        })
        .AddAccountClaimsPrincipalFactory<ArrayClaimsPrincipalFactory<RemoteUserAccount>>();
Kirstinkirstyn answered 24/12, 2020 at 15:18 Comment(1)
Thanks for this. I was finding it difficult to get Roles operating between Blazor WASM and Server Hosted on .Net 6. Just needed to Add the above including options.UserOptions.RoleClaim = "roles";Unpleasant
S
2

After understanding the problem with Steve I did the following solution. Useful for those who follow Cris Sainty's documentation

I update my method to parse claims from jwt to separate all claim's array!

private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
    var claims = new List<Claim>();
    var payload = jwt.Split('.')[1];
    var jsonBytes = ParseBase64WithoutPadding(payload);
    var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);

    keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);

    if (roles != null)
    {
        if (roles.ToString().Trim().StartsWith("["))
        {
            var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());
            foreach (var parsedRole in parsedRoles)
            {
               claims.Add(new Claim(ClaimTypes.Role, parsedRole));
            }
        }
        else
        {
            claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
        }
        keyValuePairs.Remove(ClaimTypes.Role);
    }
    claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
    for (int i = 0; i < claims.Count; i++)
    {
        var name = claims[i].Type;
        var value = claims[i].Value;
        if (value != null && value.StartsWith("["))
        {
            var array = JsonSerializer.Deserialize<List<string>>(value);
            claims.Remove(claims[i]);
            foreach (var item in array)
            {
                claims.Add(new Claim(name, item));
            }
        }
    }
    return claims;
}
Strontia answered 24/12, 2020 at 17:40 Comment(0)
F
-1

Adding to the above answers you can avoid it becoming array claims by having different keys for claims creation like this:

 var claims = new[]
       {
        new Claim("UserType1", "c"),
        new Claim("UserType2", "r")
        ....
       };
Fascista answered 17/6, 2022 at 6:20 Comment(1)
This could also be a solution, but be carful when create custom claims like this as they can be easy to forget about or loose track of. If you do decide to do this create some sort of CustomClaim type that holds your custom claims, but if not needed try and stick to the standards where possible.Kirstinkirstyn

© 2022 - 2024 — McMap. All rights reserved.