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

如何在.NET7自定义OAuth认证器中获取Azure AD应用角色到ClaimsIdentity

问题:Azure AD单租户应用自定义OAuth认证器无法获取应用角色声明

我正在开发一款对接Microsoft Azure AD认证的单租户专有应用,应用注册在客户的Azure AD中完成。已在应用配置中创建应用角色,使用MSAL结合Microsoft.Identity.Web可正常登录并获取角色声明(如PurchaseManager.Admin),但登出时会退出所有微软服务,不符合预期。

因此自行实现了OAuth2.0/OpenId认证器,该认证器可正常完成登录和登出,但无法从Azure AD获取应用角色声明。已在MicrosoftAccountOptions中添加了角色相关的ClaimActions映射,且已完成角色分配,却仍无法在ClaimsIdentity中获取到角色信息。

相关配置代码

认证服务配置

builder.Services.AddAuthentication(o => {
    o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
})
    .AddCookie(o => {
        o.Cookie.Name = $"{AppDomain.CurrentDomain.FriendlyName.Trim()}-{MicrosoftAccountDefaults.AuthenticationScheme}";
    })
    .AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, o => {
        o.ClientId = "[ClientId]";
        o.ClientSecret = "[ClientSecret]";
        o.TenantId = "[TenantId]";
        o.CallbackPath = "/signin-oidc";
    });

MicrosoftAccountOptions代码

public class MicrosoftAccountOptions : OAuthOptions {
    /// <summary>
    /// Initializes a new <see cref="MicrosoftAccountOptions"/>.
    /// </summary>
    public MicrosoftAccountOptions() {
        AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint;
        CallbackPath = new PathString("/signin-aad");
        TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint;
        UserInformationEndpoint = MicrosoftAccountDefaults.UserInformationEndpoint;
        JwksUri = MicrosoftAccountDefaults.JwksUri;

        Scope.Add(MicrosoftAccountScope.OpenId);
        Scope.Add(MicrosoftAccountScope.Profile);
        Scope.Add(MicrosoftAccountScope.Email);

        ClaimActions.MapJsonKey("sub", "sub");
        ClaimActions.MapJsonKey("iss", "iss");
        ClaimActions.MapJsonKey("cloud_instance_name", "cloud_instance_name");
        ClaimActions.MapJsonKey("cloud_instance_host_name", "cloud_instance_host_name");
        ClaimActions.MapJsonKey("cloud_graph_host_name", "cloud_graph_host_name");
        ClaimActions.MapJsonKey("msgraph_host", "msgraph_host");
        ClaimActions.MapJsonKey("aud", "aud");
        ClaimActions.MapJsonKey("exp", "exp");
        ClaimActions.MapJsonKey("iat", "iat");
        ClaimActions.MapJsonKey("auth_time", "auth_time");
        ClaimActions.MapJsonKey("acr", "acr");
        ClaimActions.MapJsonKey("nonce", "nonce");
        ClaimActions.MapJsonKey("preferred_username", "preferred_username");
        ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
        ClaimActions.MapJsonKey("tid", "tid");
        ClaimActions.MapJsonKey("ver", "ver");
        ClaimActions.MapJsonKey("at_hash", "at_hash");
        ClaimActions.MapJsonKey("c_hash", "c_hash");
        ClaimActions.MapJsonKey("email", "email");
        // 尝试获取角色但失败的代码
        ClaimActions.MapJsonKey("roles", "roles");
        ClaimActions.MapJsonKey(ClaimTypes.Role, "role");
        ClaimActions.MapJsonKey("wids", "wids");

        SaveTokens = true;
    }
}

MicrosoftAccountHandler代码

public class MicrosoftAccountHandler : OAuthHandler<MicrosoftAccountOptions> {
    public MicrosoftAccountHandler(IOptionsMonitor<MicrosoftAccountOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock) { }

    protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) {
        var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);

        var response = await Backchannel.SendAsync(request, Context.RequestAborted);

        if (!response.IsSuccessStatusCode)
            throw new HttpRequestException($"获取Microsoft账户信息出错 ({response.StatusCode}),请检查认证信息是否正确。");

        using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
        var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
        context.RunClaimActions();
        await Events.CreatingTicket(context);

        return new AuthenticationTicket(new ClaimsPrincipal(identity), properties, MicrosoftAccountDefaults.AuthenticationScheme);
    }
}

解决方法

1. 补充必要的权限范围

Azure AD不会自动返回应用角色声明,需要在MicrosoftAccountOptions中添加Graph API权限,并确保应用获得管理员同意:

// 在MicrosoftAccountOptions构造函数中添加
Scope.Add("User.Read");
Scope.Add("Directory.AccessAsUser.All");

2. 从ID Token中提取角色(最直接方案)

应用角色声明默认包含在ID Token中,但当前代码仅从UserInfo端点(/me)获取信息,该端点不会返回角色。修改MicrosoftAccountHandler的CreateTicketAsync方法,解析ID Token提取角色:

protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
{
    // 解析ID Token获取角色声明
    if (!string.IsNullOrEmpty(tokens.IdToken))
    {
        var jwtHandler = new JwtSecurityTokenHandler();
        if (jwtHandler.CanReadToken(tokens.IdToken))
        {
            var jwtToken = jwtHandler.ReadJwtToken(tokens.IdToken);
            foreach (var claim in jwtToken.Claims)
            {
                if (claim.Type == "roles")
                {
                    // roles是数组类型,拆分每个角色值
                    var roles = System.Text.Json.JsonSerializer.Deserialize<string[]>(claim.Value);
                    foreach (var role in roles)
                    {
                        if (!identity.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == role))
                        {
                            identity.AddClaim(new Claim(ClaimTypes.Role, role));
                        }
                    }
                }
                else if (!identity.HasClaim(c => c.Type == claim.Type))
                {
                    identity.AddClaim(claim);
                }
            }
        }
    }

    // 保留原有UserInfo端点调用逻辑,补充用户基础信息
    var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);

    var response = await Backchannel.SendAsync(request, Context.RequestAborted);

    if (!response.IsSuccessStatusCode)
        throw new HttpRequestException($"获取Microsoft账户信息出错 ({response.StatusCode}),请检查认证信息是否正确。");

    using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
    var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
    context.RunClaimActions();
    await Events.CreatingTicket(context);

    return new AuthenticationTicket(new ClaimsPrincipal(identity), properties, MicrosoftAccountDefaults.AuthenticationScheme);
}

3. 检查Azure AD应用注册配置

  • 确认用户/组已分配应用角色:在Azure AD控制台的企业应用 -> 你的应用 -> 用户和组中,验证目标用户已被分配对应的应用角色。
  • 确认ID Token包含角色声明:在应用注册的令牌配置中,检查是否已添加roles作为可选声明(若默认未包含)。

4. 修正ClaimActions映射(可选)

如果后续需要从其他来源获取角色数组,可修正ClaimActions的映射方式:

// 替换原有角色映射代码
ClaimActions.MapJsonKey(ClaimTypes.Role, "roles", JsonClaimValueTypes.JsonArray);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 10:37:52