Reputation: 1117
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> Home</MatNavItem>
<MatNavItem Href="/claims" Title="Claims"><MatIcon Icon="@MatIconNames.Vpn_key"></MatIcon> Claims</MatNavItem>
<AuthorizeView Policy="@PolicyNames.IdentitySystemAccess">
<Authorized>
<AuthorizeView Policy="@PolicyNames.AccessManagement">
<Authorized>
<MatNavSubMenu @bind-Expanded="@_accessSubMenuState">
<MatNavSubMenuHeader>
<MatNavItem AllowSelection="false"> Access Management</MatNavItem>
</MatNavSubMenuHeader>
<MatNavSubMenuList>
<AuthorizeView Policy="@PolicyNames.User">
<Authorized>
<MatNavItem Href="users" Title="users"><MatIcon Icon="@MatIconNames.People"></MatIcon> Users</MatNavItem>
</Authorized>
</AuthorizeView>
<AuthorizeView Policy="@PolicyNames.Role">
<Authorized>
<MatNavItem Href="roles" Title="roles"><MatIcon Icon="@MatIconNames.Group"></MatIcon> 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.
Upvotes: 6
Views: 1604
Reputation: 75
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")
....
};
Upvotes: -1
Reputation: 138
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;
}
Upvotes: 2
Reputation: 1117
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>>();
Upvotes: 4