Running Kentico in True Mixed-Mode Authentication

Posted by Dave Conder on July 27, 2016

Recently, we were faced with an interesting challenge: to create a Kentico site that runs in TRUE mixed-mode authentication. This means that on the same site, some users use anonymous/Forms Authentication, while others use Integrated Windows Authentication. Achieving this setup was not as simple as it sounds! Ultimately though, we were able to come up with a great solution…read on to learn how we did it.

The Challenge

As we were building a Kentico-based Intranet application, our client’s IT department provided us with a unique authentication requirement: all users on the local network needed to be authenticated automatically using Integrated Windows Authentication (IWA) via IE or Chrome (Edge doesn’t support Windows Authentication). However, in addition, all external users would need to enter their AD credentials on a friendly, branded login screen.  

Initially, this appeared it would be a fairly straightforward “Mixed Mode” configuration, but it turned out to be more complex than we expected. Kentico provides the concept of “Mixed Mode” authentication (here’s the documentation), but this is not the same as using different authentication mechanisms. It’s really just a way to allow the SQL and AD security providers to both be used, but in both cases, Form Authentication is the mechanism being used.  

The real issue we faced is that IWA is different animal altogether. It uses Kerberos to create an authentication ticket, and if successful, then all requests will have the AD user set in the HttpContext.Current.User.Identity object. While this authentication mechanism is used less frequently today than it once was, it is still a mainstay in many enterprise scenarios due to being seamless for users and a simple reliance on Active Directory to do all the heavy lifting. 

The Solution

After testing various approaches, we found a solution to this issue that works perfectly. It's somewhat complicated, but I've outlined all the steps below in detail:

  1. Make an IIS metabase change to allow anonymous/Forms Authentication and Integrated Windows Authentication (IWA) to work on the same site at the same time

  2. Configure Forms authentication on the site and set the login page to ~/ADAuth.aspx

    1. Add a <location> section for ADAuth.aspx that disables anonymous authentication and enables IWA

  3. Add code to the ADAuth.aspx page to redirect users to the home page. This code will only execute if the IWA process is successful

    1. If IWA is unsuccessful, then the ADAuth.aspx page will never be loaded because the user will be redirected to the login page configured in Kentico

  4. Add a global Application Request handler to handle the case of a 401 challenge, which only happens if IWA is enabled

  5. Add a global Authentication handler to do the actual authentication process against Kentico’s membership provider

While it may seem like a long list, it’s ultimately not very difficult. Note that testing can be a challenge, since you need a client machine that is on an Active Directory domain as well as has IE or Chrome installed with IWA enabled.

Here is a detailed description of each step:

1.    IIS Metabase change

Like any ASP.NET application, Kentico can run in this mode, but there is a big problem when you mix Forms Authentication with IWA. Basically, in order for Forms Authentication to work, it must be enabled at the <system.web> level in the web.config, and anonymous access to the login page is required, otherwise users can’t even type in their credentials. 

Here’s the problem with that: if Anonymous access is enabled at all, then the IWA token will never be sent. It’s impossible for that to happen, as all IWA-compatible browsers simply refuse to send any authentication information if anonymous access is allowed.

The obvious approach would seem to be to create a landing page for internal visitors that has anonymous authentication disabled and IWA enabled. However, if you try to do this, you’ll see an error, because by default, IIS does not allow mixed authentication modes in the same site. The fix here is to make an IIS metabase change. Doing so isn't usually isn’t ideal, but in this scenario, it’s required. To make the change, follow these steps:   

  1. Go to “Configuration Editor” at the top level
  2. In Configuration Editor, browse to the system.webServer/security/authentication/anonymousAuthentication section
  3. On the right side, hit the “Unlock Section” link
  4. Do the same thing with the section system.webServer/security/authentication/windowsAuthentication

2.  Configure Forms authentication on the site

Once this is done, the following web.config settings can be applied:  


  <system.web>
      <authentication mode="Forms">
          <forms loginUrl="/ADAuth.aspx" defaultUrl="Default.aspx" name=".ASPXFORMSAUTH" timeout="60000" slidingExpiration="true"/>
      </authentication>
  </system.web>

  <location path="ADAuth.aspx">
      <system.web>
          <authorization>
              <deny users="?"/>
              <allow users="*"/>
          </authorization>
      </system.web>
      <system.webServer>
          <security>
              <authentication>
                  <anonymousAuthentication enabled="false"/>
                  <windowsAuthentication enabled="true"/>
              </authentication>
          </security>
      </system.webServer>
  </location>

 

These settings will cause the following actions:

  • All unauthenticated users will be directed to the ~/ADAuth.aspx page
  • IWA users will trigger the IWA process
  • Forms Authentication users won’t trigger the process, and are then redirected to ~/Login.aspx

3. Add ADAuth.aspx Code

 There a few more things that need to be done to get this to work:   On ADAuth.aspx.cs add the following code:


 
   protected void Page_Load(object sender, EventArgs e)
        {
            FormsAuthentication.SetAuthCookie(MembershipContext.AuthenticatedUser.UserName, false);
            Redirect();
        }

        protected void Redirect()
        {
            var returnUrl = QueryHelper.GetString(" ReturnUrl & quot;, string.Empty);

            if (!string.IsNullOrEmpty(returnUrl))
            {
                Response.Redirect(returnUrl);
            }
            else
            {
                Response.Redirect(" ~/ ");
            }
        }
 

 

Here’s what’s happening here: The IWA authentication process actually happens before Page_Load. If the user is successfully authenticated, then a Forms Authentication cookie is set using the IWA credentials. If IWA fails or is not enabled, Page_Load will never happen, because the built-in Forms Authentication process built into Kentico will redirect the user to the specified login page (in this case, Logon.aspx, which contains a standard logon webpart from Kentico).  

4. Add Global Request Handler

Once all this is in place, you will need to add the following code, which creates an event handler for Application Requests in a Kentico Global Event Handler. Note that this handles the Request.End event. If IWA is enabled on a given page, a 401 challenge response will be sent. If this happens, we know that IWA is working.

There is also an added security check that looks to see if the current server IP matches a configuration setting. If this is true, then the user is coming from an internal IP and IWA is enabled.


 
        public override void Init()
        {
            RequestEvents.End.Execute += Req_Execute;
        }

 
        /// <summary>
        /// Used to handle Windows auth requests and redirect them if the browser is not supplying negotiate credentials
        /// </summary>
        private void Req_Execute(object sender, EventArgs e)
        {
            var resp = HttpContext.Current.Response;
            if (resp.StatusCode == 401 && HttpContext.Current.Request.Url.LocalPath.ToLower().Contains("adauth.aspx"))
            {
                var serverIP = HttpContext.Current.Request.ServerVariables["LOCAL_ADDR"];

                if (serverIP != ValidationHelper.GetString(ConfigurationManager.AppSettings["InternalServerIP"], string.Empty))
                {
                    EventLogProvider.LogInformation("AD Authentication", "Hosted IP does not match, Redirect to Logon", String.Format("Local_Addr:{0}, Configured IP: {1}", serverIP, ConfigurationManager.AppSettings["InternalServerIP"]));
                 resp.Clear();
                    resp.Redirect("/login.aspx?ReturnUrl=" + QueryHelper.GetString("ReturnUrl", "/"));
                }
            }
        }
 


 

5. Add an Authenication Handler to validate the user

The last piece of the puzzle is to create a global event handler for the Authenticate Event. This will check to see if IWA is being used, and if it is, it will extract the username from the token, match it against an AD user, and ensure that the user is valid. If IWA is not being used, it will proceed to authenticate the user normally.

 
  public override void Init()
        {
            // Assigns a handler to the SecurityEvents.Authenticate.Execute event
            // This event occurs when users attempt to log in on the website
            SecurityEvents.Authenticate.Execute += OnAuthentication;
        }


        private void OnAuthentication(object sender, AuthenticationEventArgs e)
        {
            string username = string.Empty;
            bool windowsAuth = false;

            UserInfo user = null;

            // If the user is coming in with Windows Auth, it will already be authenticated but we need to map the user to Kentico
            if (HttpContext.Current.User.Identity.AuthenticationType != "Forms")
            {

                if (HttpContext.Current.User.Identity != null && HttpContext.Current.User.Identity.IsAuthenticated)
                {
                    windowsAuth = true;
                    // User is authed from Windows Authentication
                    var userParts = HttpContext.Current.User.Identity.Name.Split('\\');
                    if (userParts.Length == 2)
                    {
                        username = userParts[1];
                        // Set the user, could be null if it doesn't exist in Kentico yet
            
                        user = UserInfoProvider.GetUserInfo(username);
                        EventLogProvider.LogInformation("AD Auth Handler", "AuthenticateWindows", string.Format("User {0} hit the Windows Auth Handler. Original name {1}", username, HttpContext.Current.User.Identity.Name));
                    }
                    else
                    {
                        // The Windows Auth user is invalid, Invalid login.
                        e.User = null;
                        return;
                    }
                }
            }

            if (!windowsAuth)
            {
                //EventLogProvider.LogInformation("AD Auth Handler", "AuthenticateForms", string.Format("User {0} hit the Kentico Auth Handler", e.UserName));
                username = e.UserName;
                // update actual information if the user is from active directory
                if (ADIntegrationProvider.IsUserAuthenticatedInAD(username, e.Password))
                {
                    var nodeAD = ADIntegrationProvider.GetADNode(username);
                    if (!ADIntegrationProvider.IsActive(nodeAD))
                    {
                        throw new ADNotActiveAccountException();
                    }
                }
                else
                {
                    // User did not exist, and AD Auth failed. Invalid login.
                    e.User = null;
                    return;
                }
            }

            // If user was not loaded from Windows auth, try to load
            if (user == null)
            {
                user = UserInfoProvider.GetUserInfo(username);
            }

            if (user == null)
            {
                // Create user, save to DB, then process
                // saving the pwd as a guid.
                user = new UserInfo();
                user.UserName = username;
                UserInfoProvider.SetUserInfo(user);
                UserInfoProvider.SetPassword(user, System.Guid.NewGuid().ToString());
                UserInfoProvider.AddUserToSite(user.UserName, SiteContext.CurrentSiteName);
                EventLogProvider.LogInformation("AD Auth Handler", "New User Created", string.Format("User {0} was created by the Auth Handler.", username));
            }

            if (user.InitFromAD())
            {
                (new ADGroupToRoleSyncTask()).UpdateUserRolesFromGroups(user);
                e.User = user;
            }

            // if user is not in active directory then not authorize
            e.User = user;
        }

 

In Conclusion

By following the above steps, you'll be able to configure a Kentico site to run in TRUE mixed-mode: Forms Authentication and Integrated Windows Authentication on the same site. This is a great set-up for internal users, since they never have to log in.

This was a really interesting challenge, and we found it very satisfying to architect a solution for it.
I hope you've found it helpful and informative, and the BlueModus team would be happy to discuss it in greater detail if you have any questions. Just email me at dconder@bluemodus.com, or hit me on Twitter at @davidconder. 


Dave Conder
Dave Conder has been using technology to create value and solve problems for more than 15 years. His background includes technical management, network architecture and design, security, and application development. As CTO, Dave ensures that BlueModus always uses the best people, practices, and tools to deliver world-class digital marketing solutions.