从 Microsoft 登录返回时,IIS 子应用程序将忽略 Azure Active Directory ReturnUri

tha*_*guy 5 authentication asp.net-mvc oauth-2.0 asp.net-mvc-5 azure-active-directory

背景故事

我有一个现有的 ASP.NET MVC5 应用程序,我正在连接它以使用 oauth2/Azure AD 身份验证。实际上我已经在多个环境中工作了(dev/qa/prod/localhost)。

在 Azure 中,我有一个具有多个返回 URI 的应用程序注册,示例:

我的部署管道设置了一个配置变量来更改 oauth 代码的 ReturnURI,并且一切正常。

以下是代码的设置方式。我的内部控制器有一个 Authorize 属性,当用户未经身份验证时,会将其重定向到/Account/Index进行 oauth 质询的位置。以下是我提出 oauth 挑战的方法:

HttpContext.GetOwinContext().Authentication.Challenge(
    new AuthenticationProperties
    {
        RedirectUri = Url.Action("ValidateOAuth", "Account", new { returnUrl })
    },
    OpenIdConnectAuthenticationDefaults.AuthenticationType);
Run Code Online (Sandbox Code Playgroud)

用户将被重定向到 Microsoft 进行登录,然后返回到/Account/ValidateOAuth. 在该操作中,我检查Request.IsAuthenticated,如果是真的,我会构建一个本地会话变量来获取有关用户的额外信息,并将它们推送到内部页面。

问题

我在特定场景中遇到了问题。在我的 QA 环境中,我托管了两个网站副本。一个位于基本 URI ( https://mywebsite.qa.com),另一个作为 IIS 中的子应用程序位于https://mywebsite.qa.com/staging。这样我们的测试人员就可以将分支部署到 /staging 站点并测试它们,而不会干扰我们通常的 QA 用户。

当用户访问临时站点时,他们会被重定向到我的/staging/Account/Index操作,而质询会将他们重定向到 Azure AD 登录站点。在那里,一旦他们进行了身份验证,他们就会被重定向回我的网站。然而,它们没有按/staging/Account/ValidateOAuth预期重定向。相反,它们被重定向到/. 这导致他们经历与主 QA 站点相同的身份验证周期。

通过设置 IIS Express 在本地运行时,我可以重现此情况,将站点托管http://localhost:43000/staginghttp://localhost:43000. 我看到它会重定向到的完全相同的行为,/并且收到错误,因为我没有本地托管的网站。

这是我的 Startup.cs 和 oauth 配置:

public class Startup
{
    // The Client ID is used by the application to uniquely identify itself to Azure AD.
    string clientId = ConfigurationManager.AppSettings["aad:ClientId"];

    string clientSecret = ConfigurationManager.AppSettings["aad:ClientSecret"];

    // RedirectUri is the URL where the user will be redirected to after they sign in.
    string redirectUri = ConfigurationManager.AppSettings["aad:RedirectUri"];

    // Tenant is the tenant ID (e.g. contoso.onmicrosoft.com, or 'common' for multi-tenant)
    static string tenant = ConfigurationManager.AppSettings["aad:Tenant"];

    // Authority is the URL for authority, composed by Microsoft identity platform endpoint and the tenant name (e.g. https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0)
    string authority = string.Format(CultureInfo.InvariantCulture, ConfigurationManager.AppSettings["aad:Authority"], tenant);

    static string graphScopes = ConfigurationManager.AppSettings["aad:GraphScopes"];

    /// <summary>
    /// Configure OWIN to use OpenIdConnect 
    /// </summary>
    /// <param name="app"></param>
    public void Configuration(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            CookieManager = new SystemWebCookieManager()
        });

        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                ClientId = clientId,
                Authority = authority,
                Scope = $"openid email profile offline_access {graphScopes}",
                RedirectUri = redirectUri,
                PostLogoutRedirectUri = redirectUri,
                TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true
                },
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = OnAuthenticationFailed,
                    AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
                }
            }
        );
    }

    /// <summary>
    /// Handle failed authentication requests by redirecting the user to the home page with an error in the query string
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage,
        OpenIdConnectAuthenticationOptions> notification)
    {
        notification.HandleResponse();
        notification.Response.Redirect("/Errors/Error?message=" + notification?.Exception?.Message);
        return Task.FromResult(0);
    }

    private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
    {
        var secret = clientSecret;

        if (string.IsNullOrEmpty(secret))
        {
            string filePath = HostingEnvironment.MapPath(@"~/local.aadclientsecret.txt");
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException("AAD Client Secret not found in Web.Config and local.aadclientsecret.txt not found on server!");
            }

            secret = File.ReadAllText(filePath);

            if (string.IsNullOrEmpty(secret))
            {
                throw new SettingsPropertyNotFoundException("AAD Client not found in Web.Config and local.aadclientsecret.txt was empty!");
            }
        }

        var idClient = ConfidentialClientApplicationBuilder.Create(clientId)
            .WithTenantId(tenant)
            .WithRedirectUri(redirectUri)
            .WithClientSecret(secret)
            .Build();

        try
        {
            string[] scopes = graphScopes.Split(' ');

            var result = await idClient.AcquireTokenByAuthorizationCode(
                scopes, notification.Code).ExecuteAsync();

            var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);

            string alias = "";
            object aliasObj = null;
            if (userDetails.AdditionalData.TryGetValue(GraphHelper.Attributes.Alias, out aliasObj))
            {
                alias = aliasObj.ToString();
            }

            // Create a new identity and copy all the claims.
            // Add in extra claims that are needed.
            var id = new ClaimsIdentity(notification.AuthenticationTicket.Identity.AuthenticationType);
            id.AddClaims(notification.AuthenticationTicket.Identity.Claims);
            id.AddClaim(new Claim("alias", alias));

            notification.AuthenticationTicket = new AuthenticationTicket
            (
                 new ClaimsIdentity(id.Claims, notification.AuthenticationTicket.Identity.AuthenticationType),
                 notification.AuthenticationTicket.Properties
            );
        }
        catch (MsalException ex)
        {
            string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
            notification.HandleResponse();
            notification.Response.Redirect($"/Errors/Error?message={message}&&debug={ex.Message}");
        }
        catch (Microsoft.Graph.ServiceException ex)
        {
            string message = "GetUserDetailsAsync threw an exception";
            notification.HandleResponse();
            notification.Response.Redirect($"/Errors/Error?message={message}&debug={ex.Message}");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我在通知中放置断点,则两者都不会被命中(如预期),因为来自 Azure 的重定向甚至不会将我返回到托管于 的网站/staging

这是我的配置值(省略敏感值):

HttpContext.GetOwinContext().Authentication.Challenge(
    new AuthenticationProperties
    {
        RedirectUri = Url.Action("ValidateOAuth", "Account", new { returnUrl })
    },
    OpenIdConnectAuthenticationDefaults.AuthenticationType);
Run Code Online (Sandbox Code Playgroud)

尝试的解决方案

如果我将新的重定向 URI 添加到 Azure 应用程序注册以指向http://localhost:44300/staging,它将成功地将我返回到 /staging 子目录中的本地托管站点,但Request.IsAuthenticated始终为 false,并且我陷入无限重定向循环中。我被发送到 Azure 进行登录,它会自动将我重定向回我的站点,然后将我重定向回 Azure 登录页面,永远重复。

我也尝试在我的实时 QA 环境中执行此操作。我在 Azure 中添加了一个要访问的重定向 URI https://mywebsite.qa.com/staging,然后将我的配置中的 RedirectUri 更改为相同的。当我/staging现在访问该网站时,它具有相同的行为。我被重定向到 Azure 进行登录,然后返回到/而不是/staging/Account/ValidateOAuth.

帮助!

我花了一天的时间进行搜索和霰弹枪调试,但不知道是什么原因造成的。我究竟做错了什么?

tha*_*guy 2

这就是我最终所做的。

我的redirectUri 指向我的域的根目录https://mywebsite.qa.com

在 Azure 中,我有多个重定向 URI,包括https://mywebsite.qa.comhttps://mywebsite.qa.com/staging

然后,在我处理登录的帐户控制器中,我有一段代码可以将用户重定向到 Azure 进行登录:


if (!Request.IsAuthenticated || forceLogin)
{
    HttpContext.GetOwinContext().Authentication.Challenge(
        new AuthenticationProperties
        {
            RedirectUri = Url.Action("ValidateOAuth", "Account", new { returnUrl })
        },
        OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
else
{
    HttpContext.Response.Redirect(Url.Action("ValidateOAuth", "Account"));
}
Run Code Online (Sandbox Code Playgroud)

我不确定我之前所做的事情和现在所做的事情有什么不同,因为距离我遇到这个问题已经过去了 3 个月。

仅供参考,在 ValidateOAuth 操作中,我只检查 IsAuthenticated,然后在将用户转发到站点内部之前执行一些其他维护操作(例如在数据库中查找用户等)。