You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改IdentityServer7预定义的Client与API Scope并添加角色

在IdentityServer 7中为JWT添加角色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中。
  • 客户端必须明确配置允许访问roles scope,否则不会返回该scope的声明。

内容的提问来源于stack exchange,提问作者Ben C

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 19:02:12