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

Keycloak集成ASP.NET Core MVC时角色声明无法生效问题

Keycloak角色无法在ASP.NET Core MVC中读取的问题

生产环境已部署Keycloak服务器,ASP.NET Core MVC应用能正常重定向完成Keycloak认证,但始终无法读取Token中的角色信息。Keycloak返回的Token里明确包含roles字段,尝试了自定义角色转换,但principal.identity中始终获取不到roles属性。

Startup.cs 配置

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.LoginPath = "/Account/Login";
})
.AddOpenIdConnect(options =>
{
    options.Authority = "https://MyServer/auth/realms/ATG";
    options.MetadataAddress = "https://MyServer/realms/ATG/.well-known/openid-configuration";
    options.ClientId = "ATG.ad.yaskawa.com";
    options.ClientSecret = "tpdyzbDOADYdsCUaoFz9bTJNqRsOsrcQ";
    options.ResponseType = "code";
    options.SaveTokens = true;
    
    options.Scope.Add("openid");
    options.CallbackPath = "/signin-oidc"; // Update callback path
    options.SignedOutCallbackPath = "/signout-callback-oidc"; // Update signout callback path
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "preferred_username",
        RoleClaimType = "roles"
    };
});
builder.Services.AddTransient<IClaimsTransformation, CustomRoleClaimsTransformation>();
//Fix Telerik camelCase to PascalCase
builder.Services.AddControllersWithViews().AddJsonOptions(options =>
                options.JsonSerializerOptions.PropertyNamingPolicy = null); ;
ConfigurationManager configuration = builder.Configuration;

IdentityModelEventSource.ShowPII = true;
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

Keycloak返回的Token

{
  "exp": 1720548651,
  "iat": 1720548351,
  "auth_time": 1720548351,
  "jti": "b9aca179-91c9-4528-834e-29f5d33e0308",
  "iss": "https://MyServer/realms/ATG",
  "aud": "account",
  "sub": "1ab9a926-d2a9-4941-9a01-ffe07d500abd",
  "typ": "Bearer",
  "azp": "ATG.ad.yaskawa.com",
  "sid": "fd0c995d-3ec7-4bec-8a0c-18449b393ee8",
  "acr": "1",
  "scope": "email profile",
  "email_verified": true,
  "roles": [
    "Admin",
    "view-profile"
  ],
  "name": "Eric Obermuller",
  "preferred_username": "myEmail@yaskawa.com",
  "given_name": "Eric",
  "family_name": "O",
  "email": "myEmail@yaskawa.com"
}

自定义角色转换类

public class CustomRoleClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var identity = (ClaimsIdentity)principal.Identity;

        // Ensure roles are being extracted and mapped correctly
        var roles = identity.FindAll("roles").Select(c => c.Value).ToList();

        foreach (var role in roles)
        {
            identity.AddClaim(new Claim(ClaimTypes.Role, role));
        }

        return Task.FromResult(principal);
    }
}

问题排查与解决步骤

1. 补充请求Scope

从返回的Token可以看到,实际请求的Scope是email profile,并没有包含角色相关的Scope。需要在OpenIdConnect配置中添加roles或者客户端对应的Scope:

options.Scope.Add("roles");
// 如果是客户端级别的角色,还需要添加客户端ID作为Scope
options.Scope.Add("ATG.ad.yaskawa.com");

2. 修正Authority路径一致性

当前Authority配置为https://MyServer/auth/realms/ATG,但MetadataAddress是https://MyServer/realms/ATG/.well-known/openid-configuration,两者路径不一致(多了/auth),可能导致元数据解析异常。统一路径:

options.Authority = "https://MyServer/realms/ATG";
// 无需手动设置MetadataAddress,会自动从Authority生成

3. 在Token验证事件中手动提取角色

如果默认的TokenValidationParameters无法正确解析数组类型的roles Claim,可以通过OpenIdConnect的OnTokenValidated事件手动提取并添加:

options.Events = new OpenIdConnectEvents
{
    OnTokenValidated = context =>
    {
        if (context.SecurityToken is JwtSecurityToken jwtToken)
        {
            // 直接从JWT Token中获取roles数组
            var roleClaims = jwtToken.Claims.Where(c => c.Type == "roles");
            var identity = (ClaimsIdentity)context.Principal.Identity;
            
            foreach (var roleClaim in roleClaims)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, roleClaim.Value));
            }
        }
        return Task.CompletedTask;
    }
};

4. 检查ClaimsTransformation的执行前提

在自定义转换类中,先打印所有Claim确认roles是否存在:

public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
    var identity = (ClaimsIdentity)principal.Identity;
    
    // 打印所有Claim,排查是否有roles字段
    foreach (var claim in identity.Claims)
    {
        Console.WriteLine($"{claim.Type}: {claim.Value}");
    }
    
    var roles = identity.FindAll("roles").Select(c => c.Value).ToList();
    foreach (var role in roles)
    {
        identity.AddClaim(new Claim(ClaimTypes.Role, role));
    }

    return Task.FromResult(principal);
}

5. 验证Keycloak客户端配置

登录Keycloak后台检查客户端设置:

  • 确认客户端Access Type为confidential(与使用ClientSecret的配置匹配)
  • 开启Full Scope Allowed,确保角色能被包含在Token中
  • 确认用户已被正确分配对应角色

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 04:00:58