在 ASP.NET Core 2.0 中使用带有身份模型的 Azure Active Directory OAuth
Posted
技术标签:
【中文标题】在 ASP.NET Core 2.0 中使用带有身份模型的 Azure Active Directory OAuth【英文标题】:Using Azure Active Directory OAuth with Identity Model in ASP.NET Core 2.0 【发布时间】:2018-04-21 15:12:24 【问题描述】:问题陈述
我们正在开发一个新的企业级应用程序,并希望利用 Azure Active Directory 登录应用程序,这样我们就不必创建另一组用户凭据。但是,我们针对此应用程序的权限模型比通过 AAD 内部的组处理的要复杂。
思想
当时的想法是,除了 ASP.NET Core 身份框架之外,我们还可以使用 Azure Active Directory OAuth 2.0 来强制用户通过 Azure Active Directory 进行身份验证,然后使用身份框架来处理授权/权限。
问题
您可以使用 Azure OpenId 身份验证开箱即用地创建项目,然后您可以使用身份框架轻松地将 Microsoft 帐户身份验证(非 AAD)添加到任何项目。但是没有内置任何东西可以将 AAD 的 OAuth 添加到身份模型中。
在尝试破解这些方法以使它们按我需要的方式工作后,我终于尝试自制我自己的解决方案,构建基于 OAuthHandler
和 OAuthOptions
类。
我在这条路线上遇到了很多问题,但我设法解决了大部分问题。现在我已经到了从端点取回令牌的地步,但我的 ClaimsIdentity 似乎无效。然后,当重定向到 ExternalLoginCallback 时,我的 SigninManager 无法获取外部登录信息。
几乎可以肯定,我错过了一些简单的东西,但我似乎无法确定它是什么。
代码
Startup.cs
services.AddAuthentication()
.AddAzureAd(options =>
options.ClientId = Configuration["AzureAd:ClientId"];
options.AuthorizationEndpoint = $"Configuration["AzureAd:Instance"]Configuration["AzureAd:TenantId"]/oauth2/authorize";
options.TokenEndpoint = $"Configuration["AzureAd:Instance"]Configuration["AzureAd:TenantId"]/oauth2/token";
options.UserInformationEndpoint = $"Configuration["AzureAd:Instance"]Configuration["AzureAd:TenantId"]/openid/userinfo";
options.Resource = Configuration["AzureAd:ClientId"];
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
);
AzureADExtensions
namespace Microsoft.AspNetCore.Authentication.AzureAD
public static class AzureAdExtensions
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => );
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
return builder.AddOAuth<AzureAdOptions, AzureAdHandler>(AzureAdDefaults.AuthenticationScheme, AzureAdDefaults.DisplayName, configureOptions);
public static ChallengeResult ChallengeAzureAD(this ControllerBase controllerBase, SignInManager<ApplicationUser> signInManager, string redirectUrl)
return controllerBase.Challenge(signInManager.ConfigureExternalAuthenticationProperties(AzureAdDefaults.AuthenticationScheme, redirectUrl), AzureAdDefaults.AuthenticationScheme);
AzureAD 选项和默认值
public class AzureAdOptions : OAuthOptions
public string Instance get; set;
public string Resource get; set;
public string TenantId get; set;
public AzureAdOptions()
CallbackPath = new PathString("/signin-azureAd");
AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint;
TokenEndpoint = AzureAdDefaults.TokenEndpoint;
UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint;
Scope.Add("https://graph.windows.net/user.read");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "unique_name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "given_name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "family_name");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/groups", "groups");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/identity/claims/objectidentifier", "oid");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "roles");
public static class AzureAdDefaults
public static readonly string DisplayName = "AzureAD";
public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize";
public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
public static readonly string UserInformationEndpoint = "https://login.microsoftonline.com/common/openid/userinfo"; // "https://graph.windows.net/v1.0/me";
public const string AuthenticationScheme = "AzureAD";
AzureADHandler
internal class AzureAdHandler : OAuthHandler<AzureAdOptions>
public AzureAdHandler(IOptionsMonitor<AzureAdOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
HttpResponseMessage httpResponseMessage = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (!httpResponseMessage.IsSuccessStatusCode)
throw new HttpRequestException(message: $"Failed to retrived Azure AD user information (httpResponseMessage.StatusCode) Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled.");
JObject user = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync());
OAuthCreatingTicketContext context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, user);
context.RunClaimActions();
await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("grant_type", "authorization_code");
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("client_secret", Options.ClientSecret);
dictionary.Add(nameof(code), code);
dictionary.Add("resource", Options.Resource);
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequestMessage.Content = new FormUrlEncodedContent(dictionary);
HttpResponseMessage response = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (response.IsSuccessStatusCode)
return OAuthTokenResponse.Success(JObject.Parse(await response.Content.ReadAsStringAsync()));
return OAuthTokenResponse.Failed(new Exception(string.Concat("OAuth token endpoint failure: ", await Display(response))));
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("scope", FormatScope());
dictionary.Add("response_type", "code");
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("state", Options.StateDataFormat.Protect(properties));
dictionary.Add("resource", Options.Resource);
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, dictionary);
private static async Task<string> Display(HttpResponseMessage response)
StringBuilder output = new StringBuilder();
output.Append($"Status: response.StatusCode ;");
output.Append($"Headers: response.Headers.ToString() ;");
output.Append($"Body: await response.Content.ReadAsStringAsync() ;");
return output.ToString();
AccountController.cs
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> SignIn()
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account");
return this.ChallengeAzureAD(_signInManager, redirectUrl);
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
if (remoteError != null)
_logger.LogInformation($"Error from external provider: remoteError");
return RedirectToAction(nameof(SignedOut));
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null) //This always ends up true!
return RedirectToAction(nameof(SignedOut));
你有它!
这是我拥有的代码,我几乎可以肯定,在这一点上,我缺少一些简单的东西,但不确定它是什么。我知道我的 CreateTicketAsync 方法也有问题,因为我没有点击正确的用户信息端点(或正确点击它),但这是另一个问题,因为据我了解,我关心的声明应该作为令牌。
任何帮助将不胜感激!
【问题讨论】:
【参考方案1】:我最终解决了我自己的问题,因为它最终是几个问题。我为资源字段传递了错误的值,没有正确设置我的 NameIdentifer 映射,然后有错误的端点来下拉用户信息。用户信息片段最大,因为这是我发现外部登录片段正在寻找的令牌。
更新代码
Startup.cs
services.AddAuthentication()
.AddAzureAd(options =>
options.ClientId = Configuration["AzureAd:ClientId"];
options.AuthorizationEndpoint = $"Configuration["AzureAd:Instance"]Configuration["AzureAd:TenantId"]/oauth2/authorize";
options.TokenEndpoint = $"Configuration["AzureAd:Instance"]Configuration["AzureAd:TenantId"]/oauth2/token";
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
);
AzureAD 选项和默认值
public class AzureAdOptions : OAuthOptions
public string Instance get; set;
public string Resource get; set;
public string TenantId get; set;
public AzureAdOptions()
CallbackPath = new PathString("/signin-azureAd");
AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint;
TokenEndpoint = AzureAdDefaults.TokenEndpoint;
UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint;
Resource = AzureAdDefaults.Resource;
Scope.Add("user.read");
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName");
ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName");
ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname");
ClaimActions.MapJsonKey(ClaimTypes.MobilePhone, "mobilePhone");
ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value<string>("mail") ?? user.Value<string>("userPrincipalName"));
public static class AzureAdDefaults
public static readonly string DisplayName = "AzureAD";
public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize";
public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
public static readonly string Resource = "https://graph.microsoft.com";
public static readonly string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me";
public const string AuthenticationScheme = "AzureAD";
【讨论】:
非常感谢您分享您的工作。我只需稍作修改即可移植到 .Net Core 2.1 应用程序以上是关于在 ASP.NET Core 2.0 中使用带有身份模型的 Azure Active Directory OAuth的主要内容,如果未能解决你的问题,请参考以下文章
ASP.NET Core 2.0 中RequiredAttribute 的本地化
在 HTTPS 上使用 Windows 身份验证首次调用 ASP.NET Core 2.0 Web API 在 Chrome 中总是失败
同一站点中的 asp net core 2.0 JWT 和 Openid Connect 身份验证
ASP.Net Core 2.0 - 如何从中间件返回自定义 json 或 xml 响应?
ASP.NET Core 2.0 MVC 6. 如何管理每个视图的javascript文件?
将 JWT Bearer Authentication Web API 与 Asp.Net Core 2.0 结合使用的问题