在ASP.NET Core 2.0中使用Azure Active Directory OAuth和身份模型

Mic*_*ell 10 c# oauth azure azure-active-directory asp.net-core

问题陈述

我们正在开发新的企业级应用程序,并希望利用Azure Active Directory登录应用程序,以便我们不必创建另一组用户凭据.但是,我们对此应用程序的权限模型比通过AAD内部的组处理的更复杂.

那个想法

我们的想法是,除了ASP.NET核心标识框架之外,我们还可以使用Azure Active Directory OAuth 2.0来强制用户通过Azure Active Directory进行身份验证,然后使用身份框架来处理授权/权限.

问题

您可以使用Azure OpenId身份验证创建项目开箱即用,然后您可以使用Identity框架轻松地将Microsoft帐户身份验证(非AAD)添加到任何项目.但是没有内置的东西可以将AAD的OAuth添加到身份模型中.

在尝试破解这些方法以使它们像我需要的那样工作之后,我终于尝试了自己创建自己的解决方案并建立了类OAuthHandlerOAuthOptions类.

我在这条路线上遇到了很多问题,但设法解决了大部分问题.现在我到了一个点,我从端点获得一个令牌,但我的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"];
});
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

AzureADOptions和默认值

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";
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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));
        }
    }
Run Code Online (Sandbox Code Playgroud)

你有它!

这是我的代码,我几乎可以肯定,在这一点上有一些我想念的简单但不确定它是什么.我知道我的CreateTicketAsync方法也存在问题,因为我没有找到正确的用户信息端点(或正确地命中它),但这是另一个问题,因为据我所知,我关心的声明应该作为令牌.

任何帮助将不胜感激!

Mic*_*ell 3

我最终解决了自己的问题,因为它最终是几个问题。我为资源字段传递了错误的值,没有正确设置我的 NameIdentifer 映射,然后使用错误的端点来提取用户信息。用户信息部分是最大的,因为这是我发现外部登录部分正在寻找的令牌。

更新代码

启动.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"];
});
Run Code Online (Sandbox Code Playgroud)

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";
}
Run Code Online (Sandbox Code Playgroud)