Ryck
Ryck

Reputation: 97

Blazor - Identity Add roles on application startup

I started an application in Blazor .net 3.1, and I'm having a problem. I will want to add a user with an admin role (root) when starting the application. I am using EF. Adding the user works, but adding roles throws me an exception.

Image exception

System.AggregateException : 'No service for type 'Microsoft.AspNetCore.Identity.RoleManager'1[Microsoft.AspNEtCore.Identity.IdentityRole]' has been registered.ontainer is destroyed)'

I have tried different solutions, like ASP.NET Core Identity Add custom user roles on application startup, old post but I still have the same exception, on SQLite, SQL Server,...

I created a static class and in the Startup.cs I call this method.

public static class RolesData
{
    private static readonly string[] Roles = new string[] { "Admin", "Manager", "Member" };

    public static async Task SeedRoles(IServiceProvider serviceProvider)
    {
        using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var roleManager = serviceScope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
            foreach (var role in Roles)
            {
                if (!await roleManager.RoleExistsAsync(role))
                {
                    await roleManager.CreateAsync(new IdentityRole(role));
                }
            }
        }
    }
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    RolesData.SeedRoles(app.ApplicationServices).Wait();
}

If you have any suggestions I'm interested, and also if you know of a site that explains authentication with Identity, I want to understand!

Thank you for your help

Upvotes: 1

Views: 8801

Answers (3)

Ogglas
Ogglas

Reputation: 69948

Microsoft has a good guide about this:

https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-6.0&tabs=visual-studio#name-and-role-claim-with-api-authorization

In the Client app, create a custom user factory. Identity Server sends multiple roles as a JSON array in a single role claim. A single role is sent as a string value in the claim. The factory creates an individual role claim for each of the user's roles.

CustomUserFactory.cs:

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

public class CustomUserFactory
    : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
    public CustomUserFactory(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).ToArray();

            if (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;
    }
}

In the Client app, register the factory in Program.cs:

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

In the Server app, call AddRoles on the Identity builder, which adds role-related services:

using Microsoft.AspNetCore.Identity;

...

services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

API authorization options In the Server app:

  • Configure Identity Server to put the name and role claims into the ID token and access token.
  • Prevent the default mapping for roles in the JWT token handler.

Startup.cs (Program.cs in .NET6):

using System.IdentityModel.Tokens.Jwt;
using System.Linq;

...

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

Microsofts guide says Use one of the following approaches: API authorization options or Profile Service but using a Profile Service like public class ProfileService : IProfileServic only works with Authorization Code Grant and not Resource Owner Password Credentials.

See here for more info:

https://stackoverflow.com/a/74058054/3850405

In Program.cs for ASP.NET Core 6.0 or later:

using System.Security.Claims;

...

builder.Services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

In Startup.ConfigureServices for versions of ASP.NET Core earlier than 6.0:

using System.Security.Claims;

...

services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

Upvotes: 1

Brian Parker
Brian Parker

Reputation: 14523

By the error it appears you have not configured the Identity server to expose Roles.

For example in Startup.cs

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddRoles<IdentityRole>()  // <------
                .AddEntityFrameworkStores<ApplicationDbContext>();

I have a standard template with roles seeded here

It goes further to show how to enable the use of the Authorize attribute:

@attribute [Authorize(Roles = "Administrator")]

and AuthorizeView :

<AuthorizeView Roles="Administrator">
    Only Administrators can see this.<br />
</AuthorizeView>
<AuthorizeView Roles="Moderator">
    Only Moderators can see this.<br />
</AuthorizeView>
<AuthorizeView Roles="Moderator,Administrator">
    Administrators and Moderators can see this.
</AuthorizeView>

The changes I made to a standard project to enable Roles and make them visible to Blazor WebAssembly can be seen in this commit

Upvotes: 1

HMZ
HMZ

Reputation: 3127

See my answer on seeding all kinds of data in ASP.NET Core (works in 3.1) using the IEntityTypeConfiguration

I have not tried it on blazor yet but i think it might worth the try.

Note: Changes requires a db migration.

Upvotes: 0

Related Questions