Azure Active Directory ReturnUri игнорируется для дочернего приложения IIS при возвращении из входа в систему Microsoft - PullRequest
2 голосов
/ 16 октября 2019

Предыстория

У меня есть приложение ASP.NET MVC5, которое я подключаю для использования аутентификации oauth2 / Azure AD. На самом деле это уже работает в нескольких средах (dev / qa / prod / localhost).

В Azure у меня есть регистрация приложения с несколькими возвращаемыми URI, примеры:

Мой конвейер развертывания устанавливает переменную конфигурации для изменения ReturnURI длякод oauth и все работает просто отлично.

Вот как настроен код. Мои внутренние контроллеры имеют атрибут Authorize, который, когда пользователь не аутентифицирован, перенаправляет их на /Account/Index, где выполняется вызов oauth. Вот как я запускаю вызов oauth:

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

Пользователь перенаправляется в Microsoft для входа в систему и возвращается /Account/ValidateOAuth. В этом действии я проверяю Request.IsAuthenticated и, если это true , я создаю локальную переменную сеанса для дополнительной информации о пользователе и помещаю ее во внутренние страницы.

Проблема

Я столкнулся с проблемой в конкретном сценарии. В моей среде QA у меня есть две копии сайта. Один в базовом URI (https://mywebsite.qa.com) и один в качестве дополнительного приложения в IIS, расположенного в https://mywebsite.qa.com/staging. Это сделано для того, чтобы наши тестировщики могли развертывать ветви на / промежуточном сайте и тестировать их, не мешая нашим обычным пользователям.

Когда пользователь посещает промежуточный сайт, они перенаправляются на мое действие /staging/Account/Index, а вызов перенаправляет вызов. их на сайт входа Azure AD. Там, когда они аутентифицируются, они перенаправляются обратно на мой сайт. ОДНАКО, они перенаправляются не на /staging/Account/ValidateOAuth, как ожидалось. Вместо этого они перенаправляются на /. Это заставляет их проходить один и тот же цикл проверки подлинности основного сайта QA.

Я могу воспроизвести это при локальном запуске, настроив IIS Express для размещения сайта на http://localhost:43000/staging, а не на базовом URL. http://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}");
        }
    }
}

Если я поставлю точки останова в уведомлениях, ни один из них не попадет (как и ожидалось), поскольку перенаправление из Azure даже не возвращает меня на мой веб-сайт, расположенный по адресу /staging.

Вот мои значения конфигурации (чувствительные опущены):

<add key="aad:ClientId" value="*************" />
<add key="aad:Tenant" value="***********" />
<add key="aad:ClientSecret" value="***********" />
<add key="aad:Authority" value="https://login.microsoftonline.com/{0}/v2.0" />
<add key="aad:RedirectUri" value="http://localhost:44300" />
<add key="aad:GraphScopes" value="User.Read"/>

Попытка решения

Если я добавлю новый URI перенаправления в регистрацию приложения Azure, чтобы указать на http://localhost:44300/staging, он будет успешноверните меня на мой локально размещенный сайт в подкаталоге / staging, но Request.IsAuthenticated всегда ложно, и я застреваю в бесконечном цикле перенаправления. Меня отправляют в Azure для входа в систему, и он автоматически перенаправляет меня на мой сайт, который перенаправляет меня обратно на страницу входа в Azure, повторяйте навсегда.

Я также пытался сделать это в своей среде живого контроля качества. Я добавил URI перенаправления в Azure, чтобы перейти на https://mywebsite.qa.com/staging, а затем изменил RedirectUri в моей конфигурации на тот же. Когда я захожу на сайт /staging сейчас, он ведет себя так же. Меня перенаправляют в Azure для входа в систему, а затем обратно на / вместо /staging/Account/ValidateOAuth.

Help!

Я провел день в поисках и отладке ружья, нопонятия не имею, что вызывает это. Что я делаю не так?

...