与WebForms的Azure Active Directory集成在登录时获得无限循环

问题描述:

我已阅读并遵循本文使用我们的AAD(Azure Active Directory)设置站点以获取SSO(单点登录).我已经将它与localhost以及在将其发布到Azure时都可以在全新的网站中使用

I have read and followed this article to setup my site using our AAD (Azure Active Directory) to get SSO (Single Sign On.) I have gotten it to work in a brand new website both with localhost as well as when I publish it to Azure.

以下是工作版本的应用程序注册"的设置:

Here are the settings for the working version's App Registration:

品牌:

主页URL:https://<worksgood>.azurewebsites.net

身份验证:

重定向URI:

  • https://localhost:44390/
  • https://<worksgood>.azurewebsites.net/.auth/login/aad/callback
  • https://localhost:44390/
  • https://<worksgood>.azurewebsites.net/.auth/login/aad/callback

隐性授予:

ID令牌:已检查

支持的帐户类型

仅此组织目录中的帐户(我的公司-单租户)

Accounts in this organizational directory only (My Company - Single tenant)

作为公共客户端处理应用程序

当我运行应用程序时,这是callback请求.

And when I run the application here is the callback request.

如您所见,Response Header | Location看起来不错(对我而言)

As you can see the Response Header | Location looks good (to me)

以下是我正在尝试将相同逻辑集成到的网站的应用程序注册"设置:

Here are the App Registration settings for the site I am attempting to integrate this same logic into:

品牌:

主页网址:https://<notsogood>.azurewebsites.net

身份验证:

重定向URI:

  • https://localhost:54449/
  • https://<notsogood>.azurewebsites.net/.auth/login/aad/callback
  • https://localhost:54449/
  • https://<notsogood>.azurewebsites.net/.auth/login/aad/callback

隐性授予:

ID令牌:已检查

支持的帐户类型

仅此组织目录中的帐户(我的公司-单租户)

Accounts in this organizational directory only (My Company - Single tenant)

作为公共客户端处理应用程序

当我运行应用程序时,这是callback请求.

And when I run the application here is the callback request.

当我运行它时,我确实获得了AD登录屏幕,在该屏幕上输入我的AD用户和凭据.但是,它无法成功登录.

When I run it, I do get the AD login screen where I enter my AD user and creds. However, it does not successfully log me in.

如您所见,响应中的Location被更改.我确实知道此无效版本在web.config中包含authenticationauthorization部分,如果将​​loginUrl属性从/login更改为/loginklg,它将把location更改为/loginklg?ReturnUrl=%2f.auth%2flogin%2faad%2fcallback,但如果我删除该部分,则该网站将无法正常工作.

As you can see, the Location in the response gets altered. I do know that this non-working version has the authentication and authorization sections within the web.config and if I change the loginUrl attribute from /login to /loginklg it will change the location to /loginklg?ReturnUrl=%2f.auth%2flogin%2faad%2fcallback but if I remove that section the site will not work.

您还应该注意,存在一个循环,在该循环中尝试登录我,然后由于某种原因无法登录,然后再次尝试.

You should also notice that there is a loop where it attempts to log me in and then for some reason can not and then tries again.

最初,无法正常工作的网站具有以下用于身份验证的启动代码:

Initially, the not working site had the following startup code for authentication:

public void ConfigureAuth(IAppBuilder app) {
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
    app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

    app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

    app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/login"),
        Provider = new CookieAuthenticationProvider {
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
               validateInterval: TimeSpan.FromMinutes(15),
               regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)
            )
        }
    });
}

我将其保留下来并取出来,对我的结果没有影响.

I have kept this in as well as taken it out and it makes no difference in my result.

唯一的区别是工作版本是MVC,并且调用了SignIn方法.

The only real difference is that the working version is MVC and the SignIn method is called.

public void SignIn() {
    if (!Request.IsAuthenticated) {
        HttpContext.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties { RedirectUri = "/" },
                OpenIdConnectAuthenticationDefaults.AuthenticationType);
    }
}

在不工作的版本中,它是一个WebForm/Page,并且Page_Load方法被称为:

And with the not working version it is a WebForm/Page and the Page_Load method is called:

请注意 此应用程序不是我本人创建的,也不是由我的公司创建的,因此我尝试将其与一些单独的类和配置设置集成在一起,并尽可能减少代码更改. _avantiaSSOEnabled只是读取我添加的web.config中的appSettings. _openIdEnabled已经存在.

Please Note This application was not created by me nor my company, so I am trying to integrate it with simply some separate classes and config settings with the least code change as possible. The _avantiaSSOEnabled just reads an appSettings in the web.config which I added. The _openIdEnabled already existed.

_openIdEnabled = false

_avantiaSSOEnabled = true

即使我启用了_openIdEnabledLocation仍然很糟糕.

Even if I enable _openIdEnabled the Location is still bad.

protected void Page_Load(object sender, EventArgs e) {
        if (_avantiaSSOEnabled) {
                if (!Request.IsAuthenticated) {
                    Request.GetOwinContext().Authentication.Challenge(
                            new Microsoft.Owin.Security.AuthenticationProperties { RedirectUri = "/klg" },
                            OpenIdConnectAuthenticationDefaults.AuthenticationType);
                }
        }

        if (_openIdEnabled)
                openIdBackgroundSignIn.OnOpenIdSSOLoggedIn += OnOpenIdSSOLoggedIn;

        if (!IsPostBack) {
                if (SystemHub.Maintenance.IsActive)
                        HandleInfoPopup(MaintenenceException.Text, true);
                else if (Request["error"] != null)
                        HandleError(Request["error"].ToString());
                else if (Request["auto"] == "true")
                        HandleInfoPopup(AutoLogout.Text, true);
                else if (_openIdEnabled) {
                        openIdBackgroundSignIn.ClearData();
                        if (Request["oidc_error"] != null) //This is usually when auto-login fails, so we pass it to client side which will handle it
                                openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ERROR, Request["oidc_error"].ToString());
                        else if (Request["oidc_login"] == "true")
                                openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_LOGIN_SUCCESS, true);
                        else if (User.Identity.IsAuthenticated)
                                Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
                        else if (Request["lo"] == null) //lo is set when coming from logout, so don't try to autologin
                                openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ATTEMPT_LOGIN_AUTO, true);
                }
                else if (User.Identity.IsAuthenticated) {
                        Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
                }
        }
}

我所做的唯一代码更改(不在上面的链接文章中)是试图修复它,阅读了许多其他文章,并且默认cookie管理器存在一个已知问题.结果如下:

The only code change I made (that wasn't in the above linked article) was in a attempt to fix it, reading many other articles and there is a known issue with default cookie manager. Here is the result:

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

我知道我已经接近了.显然,某些事情正在拦截该请求并对其进行调整.我只是不确定在哪里看.自从开始以来我就一直在用C#进行编码,但是我不习惯C#的安全性/SSO方面,因此可以提供任何帮助.如果您需要我添加更多信息,可以,请告诉我.

I know I am close. Clearly something is intercepting the request and tweaking it. I am just not sure where to look. I have been coding in C# since the start, but I am not that used to the security/SSO side of it, so any help is appreciated. If you need me to add more information, I can, just let me know what.

更新-2020年7月31日

按照

从下图中可以看到,从AAD登录日志中,我已经成功登录.因此,似乎代码一旦登录便无法记住或存储令牌,然后再次尝试并在失败之前必须有一定的尝试次数或时间阈值.

As you can see in the image below, from the AAD Sign-in Log, I am successfully logging in. So it seems as if the code is unable to remember or store the token once logged in and then just tries again and must have some threshold of tries or time before it fails.

奇怪的是,当它停止循环时,我收到以下消息,其中包含我尝试登录的帐户的电子邮件,并显示已登录"

Oddly enough, when it stops looping I get the following message which has the email of the account I am attempting to log in as and says "Signed in"

循环问题已通过删除以下行得到解决:

The looping issues was fixed by removing the following line:

app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

之所以发生循环,是因为身份验证类型(在上一行中设置)将返回Cookies.但是,AAD的响应将类型设置为ApplicationCookie.

The looping was happening because the authentication type (set in the above line) would return Cookies. However, the response from AAD was setting the type as ApplicationCookie.

现在ConfigAuth中的完整代码为:

The full code in the ConfigAuth is now:

public void ConfigAuth(IAppBuilder app) {
  app.CreatePerOwinContext(ApplicationDbContext.Create);
  app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
  app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

  app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
  app.UseCookieAuthentication(new CookieAuthenticationOptions {
    CookieManager = new SystemWebChunkingCookieManager(),
    Provider = new CookieAuthenticationProvider {
      OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
        validateInterval: AuthenticationHelper.OpenIdEnabled
          ? TimeSpan.FromSeconds(30)
          : TimeSpan.FromMinutes(15),
        regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
  });
  app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions {
      ClientId = AvantiaSSOHelper.ClientId,
      Authority = AvantiaSSOHelper.Authority,
      PostLogoutRedirectUri = AvantiaSSOHelper.PostLogoutRedirectUri,
      Notifications = new OpenIdConnectAuthenticationNotifications {
        AuthenticationFailed = (context) => {
          context.HandleResponse();
          context.Response.Redirect("/?errormessage=" + context.Exception.Message);
          return Task.FromResult(0);
        },
        AuthorizationCodeReceived = (context) => {
          Debug.WriteLine($"Authorization code received: {context.Code}");
          return Task.FromResult(0);
        },
        MessageReceived = (context) => {
          Debug.WriteLine($"Message received: {context.Response.StatusCode}");
          return Task.FromResult(0);
        },
        SecurityTokenReceived = (context) => {
          Debug.WriteLine($"Security token received: {context.ProtocolMessage.IdToken}");
          string test = context.ProtocolMessage.AccessToken;
          return Task.FromResult(0);
        },
        SecurityTokenValidated = (context) => {
          Debug.WriteLine($"Security token validated: {context.Response.StatusCode}");
          var nameClaim = context.AuthenticationTicket.Identity.Claims
            .Where(x => x.Type == AvantiaSSOHelper.ClaimTypeWithEmail)
            .FirstOrDefault();

          if (nameClaim != null)
            context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));

          return Task.FromResult(0);
        },
        TokenResponseReceived = (context) => {
          string test = context.ProtocolMessage.AccessToken;
          return Task.FromResult(0);
        }
      }
    }
  );

  // This makes any middleware defined above this line run before the Authorization rule is applied in web.config
  app.UseStageMarker(PipelineStage.Authenticate);
}

进行了一次(非循环)调用,然后系统尝试以Authenticated模式继续.但是,我还需要再做一步.最后一步是通过将适当的响应声明添加到身份验证票证中来更改SecurityTokenValidated事件.我们的系统正在使用Micrososft Identity,因此基于电子邮件地址.因此,我需要从提取的电子邮件声明值中将ClaimTypes.Name类型的Claim添加到身份验证票证中,如下所示:

That made a single (non-looping) call and then the system attempted to continue in an Authenticated mode. However, there was still one more step I needed to do. This last step was to alter the SecurityTokenValidated event by adding the appropriate response claim into the authentication ticket. Our system is using Micrososft Identity and is thus based on an email address. So I need to add a Claim of type ClaimTypes.Name to the authentication ticket from the extracted email claims value as follows:

context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));

AvantiaSSOHelper.ClaimTypeWithEmail只是我正在读取的Web.config文件中的一个值,以防其他实现对我需要摘录的说法有所不同.

The AvantiaSSOHelper.ClaimTypeWithEmail is simply a value I am reading out of the Web.config file in case other implementations have a different claim I would need to extsract.