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

Azure Active Directory Integration with WebForms Getting Infinite Loop at Login

我已阅读并遵循 this article 使用我们的 AAD(Azure Active Directory)设置我的网站以获取 SSO(单点登录)。我已经让它在一个全新的网站上工作 localhost 以及当我将它发布到 Azure 时。


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

品牌:

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

身份验证:

重定向 URI:

隐式授予:

ID 令牌:已检查

支持的账户类型

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

将应用程序视为 public 客户端

没有

而当我运行这里的申请是callback请求。

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


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

品牌:

首页URL:https://<notsogood>.azurewebsites.net

身份验证:

重定向 URI:

隐式授予:

ID 令牌:已检查

支持的账户类型

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

将应用程序视为 public 客户端

没有

而当我运行这里的申请是callback请求。

当我 运行 它时,我确实看到了 AD 登录屏幕,我可以在其中输入我的 AD 用户和凭据。但是,它没有成功登录。

如您所见,响应中的 Location 发生了变化。我知道这个非工作版本在 web.config 中有 authenticationauthorization 部分,如果我将 loginUrl 属性从 /login 更改为 /loginklg 它会将 location 更改为 /loginklg?ReturnUrl=%2f.auth%2flogin%2faad%2fcallback 但如果我删除该部分,该网站将无法运行。

您还应该注意到有一个循环,它尝试让我登录,然后由于某种原因不能登录,然后重试。


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

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)
            )
        }
    });
}

我把它放进去,也把它取出来了,这对我的结果没有影响。

唯一真正的区别是工作版本是 MVC 并且调用了 SignIn 方法。

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

并且对于不工作的版本,它是一个 WebForm/Page 并且 Page_Load 方法被调用:

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

_openIdEnabled = false

_avantiaSSOEnabled = true

即使我启用 _openIdEnabledLocation 仍然很糟糕。

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 管理器存在一个已知问题。这是结果:

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

我知道我很接近。显然,某些东西正在拦截请求并对其进行调整。我只是不确定去哪里看。我从一开始就用 C# 编写代码,但我不太习惯它的 security/SSO 方面,所以任何帮助都将不胜感激。如果您需要我添加更多信息,我可以,让我知道什么。

更新 - 2020 年 7 月 31 日

在关注 this article 之后,我能够修复位置 /login?ReturnUrl...,如下所示。

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

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

通过删除以下行解决了循环问题:

app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

循环发生是因为身份验证类型(在上一行中设置)将 return Cookies。但是,AAD 的响应将类型设置为 ApplicationCookie

ConfigAuth 中的完整代码现在是:

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);
}

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

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

AvantiaSSOHelper.ClaimTypeWithEmail 只是我从 Web.config 文件中读出的一个值,以防其他实现有不同的声明,我需要 extsract。