Reputation: 97
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.
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
Reputation: 69948
Microsoft has a good guide about this:
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:
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
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