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

如何将自定义Claims从Duende IdentityServer发送到Blazor WASM托管应用

问题:Blazor客户端无法获取Duende IdentityServer返回的自定义权限Claim

我正在开发托管式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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 01:01:15