Search code examples
javascriptc#htmlblazor-webassemblyasp-net-core-spa-services

Problem when user has multi roles in Blazor Webassembly authentication and authorisation?


I have a Blazor Webassembly solution, when I login with user (has multi roles), login action show error:

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.ArgumentException: An item with the same key has already been added. Key: http://schemas.microsoft.com/ws/2008/06/identity/claims/role
         at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
         at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
         at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
         at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
         at REMO.Server.Controllers.AuthController.CurrentUserInfo() in D:\REMO\REMO\Server\Controllers\AuthController.cs:line 87
         at lambda_method29(Closure , Object , Object[] )
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean&
isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

Following my Custom Authentication Provider:

public class CustomStateProvider : AuthenticationStateProvider
{
        private readonly IAuthService api;
    private CurrentUser _currentUser;
    public CustomStateProvider(IAuthService api)
    {
        this.api = api;
    }
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity();
        try
        {
                var userInfo = await GetCurrentUser();
                var roleClaims = identity.FindAll(identity.RoleClaimType);
                if (userInfo.IsAuthenticated)
                
                {
                var claims = new[] 
                { 
                   new Claim(ClaimTypes.Name, _currentUser.UserName)
                }
                .Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
                    identity = new ClaimsIdentity(claims, "Server authentication");
                }
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine("Request failed:" + ex.ToString());
        }

        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

Startup.cs

...
    services.AddDbContext<ApplicationDBContext>(options => options .UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDBContext>();
...

The problem seems to be the presence of multiple roles for an identity.
In case of an identity with only one role the problem doesn't arise.


Solution

  • Your problem is the construction of the role claim key.
    This key, in case of multiple role, has to be an array.
    You cannot have multiple "role" key in your JWT token.

    I think you've used the code from Mukesh and is a good starting point, but if you read the comment, the last one explain a problem like yours.

    So you need to modify the line

    .Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
    

    extracting via LINQ all the claims that aren't of type role and add them to the claims array.
    Now you need to create an array of all claims of type role (I think they come from your API) and add a single role claim entry of array type.

    After that modification I think it should work.

    The resulting decoded JWT token should have the form:

    {
       "sub": "nbiada",
       "jti": "123...",
       "role" : [
          "User",
          "Admin"
       ],
       "exp": 12345467,
    ...
    }
    

    Note: I've shorten the role key, in your implementation should be http://schemas.microsoft.com/ws/2008/06/identity/claims/role