如何在.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
相关产品推荐
相关产品推荐

