Azure Active Directory Integration with WebForms Getting Infinite Loop at Login

余生颓废 提交于 2020-08-10 19:19:23

问题


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:

Branding:

Home page URL: https://<worksgood>.azurewebsites.net

Authentication:

Redirect URIs:

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

Implicit grant:

ID Tokens: Checked

Supported account types

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

Treat application as a public client

No

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

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:

Branding:

Home page URL: https://<notsogood>.azurewebsites.net

Authentication:

Redirect URIs:

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

Implicit grant:

ID Tokens: Checked

Supported account types

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

Treat application as a public client

No

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

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.

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.

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

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

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

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

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

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.

UPDATE - 07/31/2020

I was able to fix the Location /login?ReturnUrl... after following this article as you can see below.

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"


回答1:


The looping issues was fixed by removing the following line:

app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

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.

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

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

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.



来源:https://stackoverflow.com/questions/63140000/azure-active-directory-integration-with-webforms-getting-infinite-loop-at-login

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!