如何将自定义Claims从Duende IdentityServer发送到Blazor WASM托管应用
我正在开发托管式Blazor WASM应用,以Blazor Server作为BFF对接Duende IdentityServer实现认证授权。用户角色关联权限,我希望将权限作为Claim添加到用户信息中,用于Blazor应用的授权逻辑。已在PermissionClaimsPrincipalFactory中添加权限Claim,并在PermissionClaimProfileService中将用户所有Claim加入context.IssuedClaims。登录后,Blazor客户端仅显示默认Claims,日志显示Profile Service已返回Permissions等自定义Claim,但客户端无法获取。
现状信息
日志返回的Claim类型
sub preferred_username name amr auth_time idp email AspNet.Identity.SecurityStamp http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier TenantKey Permissions email_verified
浏览器显示的Claims
amr pwd sid 5BB444DB7141399BFA6ED4D1A65B1AE6 sub 5c009dae-d118-44e1-9a46-c119dcc31ff9 auth_time 1662538078 idp local name {Name} email {Email} bff:logout_url /bff/logout?sid=5BB444DB7141399BFA6ED4D1A65B1AE6 bff:session_expires_in 1209598 bff:session_state EvCMn29HkOmdDzxlAqvwUGfj3u0DgxHCymQtn1tOw0U.4F818675DFBEC4B689B4E3159633443A
部署地址
- Blazor应用:https://localhost:7111
- IdentityServer:https://localhost:7193
现有配置
IdentityServer客户端配置
"BlazorClient": { "AlwaysSendClientClaims": true, "ClientId": "app-blazor", "ClientSecrets": [ "SuperSecretPassword" ], "ClientName": "App", "ClientUri": "https://localhost:7111/", "AllowedGrantTypes": [ "authorization_code" ], "AllowOfflineAccess": true, "RedirectUris": [ "https://localhost:7111/signin-oidc" ], "FrontChannelLogoutUri": "https://localhost:7111/signout-oidc", "PostLogoutRedirectUris": [ "https://localhost:7111/signout-callback-oidc" ], "AllowedScopes": [ "openid", "profile", "api1" ] }
IdentityServer资源配置
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile() }; public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope> { new ApiScope("api1", "MyAPI") };
IdentityServer Profile Service
public class PermissionClaimProfileService : ProfileService<ApplicationUser> { public PermissionClaimProfileService(UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, ILogger<ProfileService<ApplicationUser>> logger) : base(userManager, claimsFactory, logger) { } public override async Task GetProfileDataAsync(ProfileDataRequestContext context) { await base.GetProfileDataAsync(context); var existingClaims = context.IssuedClaims; foreach(var claim in context.Subject.Claims) { if (!existingClaims.Select(c => c.Type).ToList().Contains(claim.Type)) existingClaims.Add(claim); } context.IssuedClaims = existingClaims.ToList(); } }
IdentityServer Claims Principal Factory
public class PermissionClaimsPrincipalFactory<TIdentityUser> : UserClaimsPrincipalFactory<TIdentityUser> where TIdentityUser : IdentityUser { private readonly IClaimsCalculator _claimsCalculator; public PermissionClaimsPrincipalFactory(UserManager<TIdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor, IClaimsCalculator claimsCalculator) : base(userManager, optionsAccessor) { _claimsCalculator = claimsCalculator; } protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TIdentityUser user) { var identity = await base.GenerateClaimsAsync(user); var userId = identity.Claims.GetUserIdFromClaims(); var claims = await _claimsCalculator.GetClaimsForUser(userId); identity.AddClaims(claims); return identity; } }
IdentityServer注册代码
services.AddScoped<IClaimsCalculator, ClaimsCalculator>(); services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, PermissionClaimsPrincipalFactory<ApplicationUser>>(); services .AddIdentityServer(options => { options.KeyManagement.Enabled = true; options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; options.EmitStaticAudienceClaim = true; }) .AddConfigurationStore(options => { options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("identityserver"), sql => sql.MigrationsAssembly(migrationsAssembly)); }) .AddOperationalStore(options => { options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("identityserver"), sql => sql.MigrationsAssembly(migrationsAssembly)); options.EnableTokenCleanup = true; options.TokenCleanupInterval = 1800; }) .AddAspNetIdentity<ApplicationUser>() .AddProfileService<PermissionClaimProfileService>();
Blazor Server启动代码
public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); builder.Services.AddBff(); builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; }) .AddOpenIdConnect("oidc", options => { options.Authority = configuration.GetValue<string>("IdentityServer:Authority"); options.ClientId = configuration.GetValue<string>("IdentityServer:ClientId"); options.ClientSecret = configuration.GetValue<string>("IdentityServer:ClientSecret"); options.ResponseType = "code"; options.ResponseMode = "query"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api1"); options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseBff(); app.UseAuthorization(); app.MapBffManagementEndpoints(); app.MapRazorPages(); app.MapControllers(); app.MapFallbackToFile("index.html"); app.Run(); }
Blazor客户端显示Claims代码
<AuthorizeView> <Authorized> <dl> @foreach (var claim in @context.User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> @if(context.User.HasPermission(Permissions.CompanyProfile_ManagePermissions)){ <p>User has permissions to manage permissions for company profile</p> } @if (context.User.HasPermission(Permissions.Property_Read)) { <p>User has permissions to read properties</p> } </Authorized> <NotAuthorized> <h3>No session</h3> </NotAuthorized> </AuthorizeView>
1. 注册自定义Identity资源
Duende IdentityServer仅返回已注册Identity资源包含的Claim类型,需将Permissions和TenantKey添加到Identity资源中:
修改IdentityServer资源配置:
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), // 新增自定义Identity资源 new IdentityResource("permissions", "User Permissions", new[] { "Permissions", "TenantKey" }) };
更新客户端配置的AllowedScopes:
"AllowedScopes": [ "openid", "profile", "api1", "permissions" // 新增 ]
在Blazor Server的OpenIdConnect配置中添加该scope:
options.Scope.Add("permissions"); // 新增
2. 优化Profile Service逻辑
调整PermissionClaimProfileService,仅返回客户端请求的Claim类型:
public override async Task GetProfileDataAsync(ProfileDataRequestContext context) { await base.GetProfileDataAsync(context); var user = await UserManager.GetUserAsync(context.Subject); var principal = await ClaimsFactory.CreateAsync(user); // 仅添加客户端请求的Claim var requestedClaims = principal.Claims .Where(c => context.RequestedClaimTypes.Contains(c.Type)) .ToList(); context.IssuedClaims.AddRange(requestedClaims); }
3. 配置BFF传递自定义Claim到WASM客户端
在Blazor Server的BFF配置中指定要传递的Claim:
builder.Services.AddBff(options => { options.ManagementBasePath = "/bff"; options.UserOptions.ClaimsRetrieval = UserClaimsRetrievalMode.UserInfoEndpoint; // 指定要传递到客户端的Claim列表 options.UserOptions.ClaimsFilter = new[] { "sub", "name", "email", "Permissions", "TenantKey" }; });
同时在OpenIdConnect配置中添加Claim映射,确保自定义Claim不被过滤:
.AddOpenIdConnect("oidc", options => { // ... 其他配置 options.ClaimActions.MapUniqueJsonKey("Permissions", "Permissions"); options.ClaimActions.MapUniqueJsonKey("TenantKey", "TenantKey"); });
4. 调整Cookie认证配置
禁用默认Claim类型映射,避免自定义Claim被重命名:
.AddCookie("cookie", options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; options.SaveTokens = true; // 清除默认Claim类型映射 options.ClaimTypeMap.Clear(); });
5. 清除缓存并重新登录
修改配置后,清除浏览器认证Cookie,重新登录以获取包含新Claim的令牌。
内容的提问来源于stack exchange,提问作者JeremyVm

