如何修改IdentityServer7预定义的Client与API Scope并添加角色
问题背景
我基于.NET Core 7、IdentityServer 7、EntityFramework 7和Angular 15搭建Web应用,当前JWT的Scope仅包含MyAppAPI、openid和profile,想要为Scope添加角色信息。尝试多种方法都指向创建新的IdentityResources、Clients和ApiScopes,但这么做会报错,因为这些资源在IdentityServer 7中已默认存在。
最近尝试在Program.cs的AddApiAuthorization<ApplicationUser, ApplicationDbContext>方法中配置选项参数,结果报错Can't determine the type for the client type,不确定是否走对了方向。
原Program.cs代码:
using Duende.IdentityServer.AspNetIdentity; using Duende.IdentityServer.EntityFramework.Entities; using Duende.IdentityServer.Models; using AdminPortal.Areas.Identity.Data; using AdminPortal.Areas.Identity.Models; using AdminPortal.Framework; using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.AzureAppServices; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); string envName = string.IsNullOrEmpty(builder.Configuration["configEnvName"]) ? "development" : builder.Configuration["configEnvName"].ToString(); builder.Configuration.AddJsonFile("appsettings.json").AddJsonFile($"appsettings.{envName}.json"); builder.Logging.AddAzureWebAppDiagnostics(); builder.Services.Configure<AzureFileLoggerOptions>(options => { options.FileName = "AdminPortal-diagnostics-"; options.FileSizeLimit = 50 * 1024; options.RetainedFileCountLimit = 5; }); builder.Services.Configure<AzureBlobLoggerOptions>(options => { options.BlobName = "log.txt"; }); // Add services to the container. var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddRoles<ApplicationRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => { options.IdentityResources = Config.IdentityResources; options.Clients = Config.Clients; options.ApiScopes = Config.ApiScopes; }) .AddProfileService<ProfileService>(); builder.Services.AddAuthentication() .AddIdentityServerJwt(); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); //builder.Services.AddScoped<IClaimsTransformation, ClaimsTransformer>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseMigrationsEndPoint(); } else { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseIdentityServer(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); app.MapRazorPages(); app.MapFallbackToFile("index.html"); ; app.Run();
原Config.cs代码:
using Duende.IdentityServer.Models; using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; using System.Collections.Generic; namespace AdminPortal.Framework { public static class Config { public static IdentityResourceCollection IdentityResources => new IdentityResourceCollection( new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), //new IdentityResources.Email(), // Can implement later if needed //new IdentityResources.Phone(), // Can implement later if needed //new IdentityResources.Address(), // Can implement later if needed new IdentityResource("roles", "User roles", new List<string> { "role" }) }); public static ApiScopeCollection ApiScopes => new ApiScopeCollection( new ApiScope[] { new ApiScope("AdminPortalAPI"), new ApiScope("openid"), new ApiScope("profile"), new ApiScope("roles") } ); public static ClientCollection Clients => new ClientCollection( new Client[] { new Client { ClientId = "AdminPortalAPI", ClientName = "AdminPortal Credentials Client", AllowedGrantTypes = GrantTypes.ClientCredentials, AccessTokenType = AccessTokenType.Jwt, ClientSecrets = { new Secret("AdminPortal_client_secret".Sha256()) }, AllowedScopes = { "AdminPortalAPI" } }, new Client { ClientId = "AdminPortal", ClientName = "AdminPortal SPA", AllowedGrantTypes = GrantTypes.Code, AccessTokenType = AccessTokenType.Jwt, RequirePkce = true, RequireClientSecret = false, AllowedScopes = { "openid", "profile", "AdminPortalAPI", "roles" }, RedirectUris = { "https://localhost:44463/auth-callback" }, PostLogoutRedirectUris = { "https://localhost:44463/" }, AllowedCorsOrigins = { "https://localhost:44463" }, AllowOfflineAccess = true } } ); } }
解决方案
核心思路
不要直接覆盖默认的IdentityResources、Clients和ApiScopes,而是扩展现有资源,同时通过ProfileService把角色声明注入到JWT中,避免重复注册默认资源导致的错误。
步骤1:修正Config.cs,移除重复的默认资源
openid、profile是IdentityServer自动注册的默认资源,不需要手动添加到ApiScopes中,只保留自定义的roles资源:
using Duende.IdentityServer.Models; using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; using System.Collections.Generic; namespace AdminPortal.Framework { public static class Config { public static IdentityResourceCollection IdentityResources => new IdentityResourceCollection( new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), // 仅添加自定义的roles身份资源 new IdentityResource("roles", "User roles", new List<string> { "role" }) }); public static ApiScopeCollection ApiScopes => new ApiScopeCollection( new ApiScope[] { new ApiScope("AdminPortalAPI"), // 移除重复的openid、profile,保留自定义roles scope new ApiScope("roles") } ); public static ClientCollection Clients => new ClientCollection( new Client[] { new Client { ClientId = "AdminPortalAPI", ClientName = "AdminPortal Credentials Client", AllowedGrantTypes = GrantTypes.ClientCredentials, AccessTokenType = AccessTokenType.Jwt, ClientSecrets = { new Secret("AdminPortal_client_secret".Sha256()) }, AllowedScopes = { "AdminPortalAPI" } }, new Client { ClientId = "AdminPortal", ClientName = "AdminPortal SPA", AllowedGrantTypes = GrantTypes.Code, AccessTokenType = AccessTokenType.Jwt, RequirePkce = true, RequireClientSecret = false, AllowedScopes = { "openid", "profile", "AdminPortalAPI", "roles" }, RedirectUris = { "https://localhost:44463/auth-callback" }, PostLogoutRedirectUris = { "https://localhost:44463/" }, AllowedCorsOrigins = { "https://localhost:44463" }, AllowOfflineAccess = true } } ); } }
步骤2:实现ProfileService注入角色声明
确保ProfileService正确读取用户角色并添加到JWT声明中:
using Duende.IdentityServer.AspNetIdentity; using Duende.IdentityServer.Models; using AdminPortal.Areas.Identity.Data; using Microsoft.AspNetCore.Identity; using System.Linq; using System.Threading.Tasks; namespace AdminPortal.Framework { public class ProfileService : ProfileService<ApplicationUser> { private readonly UserManager<ApplicationUser> _userManager; private readonly RoleManager<ApplicationRole> _roleManager; public ProfileService(UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory) : base(userManager, claimsFactory) { _userManager = userManager; _roleManager = roleManager; } public override async Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = await _userManager.GetUserAsync(context.Subject); var roles = await _userManager.GetRolesAsync(user); // 添加角色声明到JWT var roleClaims = roles.Select(r => new Claim("role", r)); context.IssuedClaims.AddRange(roleClaims); // 保留基类的默认声明逻辑 await base.GetProfileDataAsync(context); } } }
步骤3:修正Program.cs中的IdentityServer配置
使用AddIdentityResources、AddApiScopes、AddClients扩展方法替代直接覆盖选项,避免类型错误:
// 替换原有的AddApiAuthorization配置 builder.Services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>() // 添加自定义身份资源 .AddIdentityResources(Config.IdentityResources) // 添加自定义ApiScopes .AddApiScopes(Config.ApiScopes) // 添加自定义Clients .AddClients(Config.Clients) .AddProfileService<ProfileService>();
步骤4:配置Angular客户端请求roles scope
在Angular的认证配置中,确保请求的scopes包含roles,例如auth-config.ts:
export const authConfig: AuthConfig = { issuer: 'https://localhost:your-port', redirectUri: window.location.origin + '/auth-callback', clientId: 'AdminPortal', scope: 'openid profile AdminPortalAPI roles', // 包含roles scope responseType: 'code', postLogoutRedirectUri: window.location.origin, };
关键注意事项
- 禁止重复注册IdentityServer默认提供的
openid、profile等资源,否则会触发重复注册错误。 ProfileService必须正确实现,确保角色声明被注入到JWT中。- 客户端必须明确配置允许访问
rolesscope,否则不会返回该scope的声明。
内容的提问来源于stack exchange,提问作者Ben C

