Identity cannot add roles to users

Hi,
We are using Blazor WASM with multitenancy and the 'default' security. There is an issue with user roles whereby we cannot add a role to a user. When we try, we get the following error:

System.InvalidOperationException: Sequence contains more than one element.
         at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
         at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.IsInRoleAsync(TUser user, String normalizedRoleName, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Identity.UserManager`1.AddToRolesAsync(TUser user, IEnumerable`1 roles)
         at STracker.Controllers.ApplicationUsersController.Patch(String key, ExpandoObject json) in C:\Dev\stracker\server\Controllers\ApplicationUsersController.cs:line 140
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask).........................

We have three tenants and each tenant has two roles - administrator and user.

This is where it fails....

Any help to resolve this is appreciated.

Hi @simon,

We reproduced the problem and will try to address it with the next Radzen release.

1 Like

Hi,
After more investigation, it appears that the issue is with having the same role name in multiple tenants. (possibly related to the side effect seen in Multi Tenant page authentication drop down has multiple values)

This is another major multitenant limitation and makes role based authorization unscalable.

Suggestions for multitenancy features:

  1. Roles to be unique for tenant only but allow same name roles in other tenants.
  2. Username/emails for tenant only but allow same name Username/emails in other tenants.
  3. Apply tenant isolation in the database context server side.

Currently it looks like I'm going to have to ignore roles and create my own authorization :frowning_face: unless anyone has another idea (please)??

We will handle the issue with multiple roles with the same name.

1 Like

@korchev , you're the best! Thanks.

I removed all roles except for one tenant which then allowed me to add roles BUT role based page authorization still fails and redirect to unauthorized. Still investigating but I assume it's related.

We can't reproduce this problem.

Here is what we tested with:

  1. A page called AdminOnly visible to Admin role.
  2. User that has the Admin role sees it in the navigation.
  3. User that does not have it does not see it and cannot access it.

Hi @korchev , I see that there has been a fix released for Roles cannot be assigned to user in multi-tenant applications. How do I apply that update to my code? I tried running the code in the updated UI but nothing has changed in my code. Is it just a library update?

Hi @simon,

The fix in Radzen and the generated code when you run the application. Make sure you don’t have files in application ignore list to get the latest code.

I just removed the exclude list and ran in Radzen but don't see any changes showing in git other than my exclude files. Do I need to drop and re-add security in the Radzen UI?

If you have security with multi tenancy enabled:

you should have the following code in your server/Startup.cs:

1 Like

You are correct, I do have that. It seems that it sneaked in on an earlier commit :slight_smile: Thank you!

I am still having issues with authorization. I have sent a link for a test project to demonstrate.

Further to this issue, the Radzen UI did not include 'builder.Services.AddAuthorizationCore();' in Program.cs which seems to be the cause of the page authorization failing.

We can't run the application without your database. Also according to our tests and prior experience AddAuthorizationCore is not required to enable authorization.

I am attaching a test application which works as expected. To run it you need to create the Sample Radzen DB available in the new MSSQL data source screen via the Create Sample Schema button. Go edit the existing data source. It has a page called AdminOnly which is accessible only to users with the role Admin. After creating the database you need to add a tenant (or more) and add the Admin role and then a user with that role.
MultiTenantWebAssembly.zip (119.1 KB)

Here is what happens when and admin logs in
admin
Here is what happens when a regular user logs in.
user
Here is how the tenant is configured:



Screenshot 2021-10-31 at 18.52.24

The database was just created with defaults in SSMS then one table added to enable the schema to be inferred:

CREATE TABLE [dbo].[dummy](
	[col1] [nchar](10) NULL
)

Other than that everything was straight out of the box. The page authorization did not work until I added AddAuthorizationCore and, even stranger, does not work at all after changing the user roles. I've tried different browsers, clearing caches etc but cannot pinpoint the problem.
I'll update after trying your project on my pc.

Update:
Your example app worked OK until I added a second role. Now the user cannot access the AdminOnly page. There appears to be something wrong with multiple roles.

Hi @simon,

We were able to reproduce the issue and we will post more info here when we are ready with our research/fix.

I have a fix, wait 2 mins...

Program.cs:

            builder.Services.AddApiAuthorization()
                .AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();

RolesClaimsPrincipalFactory.cs (new file):

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

namespace MultiTenantWebAssembly.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)
            {
                var identity = (ClaimsIdentity)user.Identity;
                var roleClaims = identity.FindAll(identity.RoleClaimType).ToList();
                if (roleClaims != null && roleClaims.Any())
                {
                    foreach (var existingClaim in roleClaims)
                    {
                        identity.RemoveClaim(existingClaim);
                    }

                    var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
                    if (rolesElem is JsonElement roles)
                    {
                        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 is based on https://github.com/dotnet/aspnetcore/issues/21836 and code at https://github.com/javiercn/BlazorAuthRoles/blob/master/Client/Program.cs#L23-L24

Note that I had to change the identity.FindAll from the link original code to create a List instead of IEnumerable.

Thanks @simon! We will update our templates with your code and the fix will be part of our next update later this week, until then you can add the file to application ignore list.

1 Like