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

.NET8升级后OpenIddict访问令牌丢失自定义Claims问题

.NET8升级后OpenIddict自定义Claims丢失问题排查与解决建议

场景

我在ASP.NET 6服务器应用中使用OpenIddict已有一段时间,该应用负责提供认证并颁发令牌供其他ASP.NET应用使用。身份由Microsoft Identity管理,支持本地账户和Google/Microsoft等社交登录提供商,在.NET 6环境下运行完全正常。

问题

升级到.NET 8后,Authorization控制器中添加的所有自定义Claims(name、自定义claim、email)均从令牌中丢失。令牌仍能正常颁发和验证,但自定义Claims完全缺失。

详细信息

1. OpenIddict配置

services
    .AddOpenIddict()

    .AddCore(options =>
        {
            options.UseEntityFrameworkCore().UseDbContext<AuthorizationDbContext>();
            options.UseQuartz();
        })

    .AddServer(options =>
    {
        options.AllowClientCredentialsFlow();
        options.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange();
        options.AllowRefreshTokenFlow();

        options.SetTokenEndpointUris("/connect/token");
        options.SetAuthorizationEndpointUris("/connect/authorize");
        options.SetUserinfoEndpointUris("/connect/userinfo");

        var certificatesSection = configuration.GetSection("Certificates");
        string encryptionCertificatePath = certificatesSection.GetValue<string>("EncryptionCertificatePath") ?? throw new Exception("Missing EncryptionCertificatePath in appsettings.json");
        string signingCertificatePath = certificatesSection.GetValue<string>("SigningCertificatePath") ?? throw new Exception("Missing SigningCertificatePath in appsettings.json");

        X509Certificate2 encryptionCertificate = new X509Certificate2(encryptionCertificatePath, "", X509KeyStorageFlags.EphemeralKeySet);
        X509Certificate2 signingCertificate = new X509Certificate2(signingCertificatePath, "", X509KeyStorageFlags.EphemeralKeySet);

        options
            .AddEncryptionCertificate(encryptionCertificate)
            .AddSigningCertificate(signingCertificate);

            options.RegisterScopes(allScopes);

            options.UseAspNetCore()
                .EnableTokenEndpointPassthrough()
                .EnableAuthorizationEndpointPassthrough()
                .EnableUserinfoEndpointPassthrough()
                .EnableStatusCodePagesIntegration();

            options.DisableAccessTokenEncryption(); //临时关闭令牌加密以便查看内容
    })

    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });

2. Authorization控制器添加自定义Claims的代码

[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
    var request = HttpContext.GetOpenIddictServerRequest() ??
        throw new InvalidOperationException("无法获取OpenID Connect请求。");

    var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);

    if (result == null || !result.Succeeded
        || request.HasPrompt(OpenIddictConstants.Prompts.Login)
        || (request.MaxAge != null
                    && result.Properties?.IssuedUtc != null
                    && DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)
                )
    )
    {
        if (request.HasPrompt(OpenIddictConstants.Prompts.None))
        {
            return Forbid(authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties(new Dictionary<string, string?>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.LoginRequired,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户未登录。"
                }));
        }

        var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
        var parameters = Request.HasFormContentType ?
                            Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
                            Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();

        parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));

        return Challenge(
                         authenticationSchemes: IdentityConstants.ApplicationScheme,
                         properties: new AuthenticationProperties
                         {
                             RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
                         });

    }

    if (result.Principal.Identity?.Name == null)
    {
        throw new InvalidOperationException("无法获取Claims主体");
    }

    // 创建新的Claims主体
    var claims = new List<Claim>
    {
            // 必填的'subject' claim
            new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),

            // 自定义claims <---- 在.NET6正常,但.NET8中丢失
            new Claim(OpenIddictConstants.Claims.Name, result.Principal.Identity.Name)
                .SetDestinations(OpenIddictConstants.Destinations.IdentityToken, OpenIddictConstants.Destinations.AccessToken),
            new Claim( ClaimTypes.NameIdentifier, result.Principal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value)
                .SetDestinations(OpenIddictConstants.Destinations.IdentityToken, OpenIddictConstants.Destinations.AccessToken),
            new Claim("some claim", "some value").SetDestinations(OpenIddictConstants.Destinations.AccessToken),
            new Claim(OpenIddictConstants.Claims.Email, "some@email").SetDestinations(OpenIddictConstants.Destinations.IdentityToken, OpenIddictConstants.Destinations.AccessToken)
    };

    var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    claimsPrincipal.SetScopes(request.GetScopes());
    return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}

3. .NET6下的令牌内容(正常情况)

{
  "sub": "radek...........",
  "name": "radek...........",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "daa1daf9-...........",
  "some claim": "some value",
  "email": "some@email",
  "oi_prst": "postman",
  "oi_au_id": "1da899ef-...........",
  "client_id": "postman",
  "oi_tkn_id": "5f78b200-...........",
  "scope": "offline_access Pardubice_API api",
  "exp": 1737714786,
  "iss": "https://localhost:44326/",
  "iat": 1737711186
}

4. .NET8下的令牌内容(自定义Claims丢失)

{
  "sub": "radek...........",
  "oi_prst": "postman",
  "oi_au_id": "7989d993-...........",
  "client_id": "postman",
  "oi_tkn_id": "08f32dab-...........",
  "scope": "offline_access Pardubice_API api",
  "iss": "https://localhost:44326/",
  "exp": 1737715346,
  "iat": 1737711746
}

注:OpenIddict未记录任何错误或警告日志。

已尝试方案

  • 添加profile scope
  • 确保所有自定义Claims都通过SetDestinations指定了目标令牌类型
  • 调整MapInboundClaims配置
  • 清除JwtSecurityTokenHandler的默认Claim映射

以上方法均未解决问题,且升级到.NET7时自定义Claims仍能正常显示,仅.NET8出现该问题。

解决建议

  1. 升级OpenIddict版本:当前使用的OpenIddict 3.1.1是较旧版本,发布时.NET8尚未推出,存在兼容性问题是大概率事件。建议升级到支持.NET8的版本(至少4.x及以上,最新稳定版为5.x),这是解决此类兼容性问题最直接的方案。

  2. 显式注册Claims:在OpenIddict服务器配置中显式声明需要保留的自定义Claims,避免被默认过滤规则移除:

    options.AddServer(options =>
    {
        // 其他配置...
        options.RegisterClaims(OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Email, "some claim");
    });
    
  3. 验证Scope关联:确保请求的Scope包含对应标准Scope(如profile对应name、email对应email),或者将自定义Claims关联到已注册的自定义Scope中,避免因Scope不匹配导致Claims被过滤。

  4. 调试Claims生命周期:在SignIn前添加调试代码,检查claimsPrincipal中的Claims是否完整;也可通过OpenIddict的事件拦截器(如ApplyClaimsTransformation)查看Claims在生成令牌前的状态,确认是否被意外过滤。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 21:19:52