TheRelentlessDev

All things tech with a sprinkle of make

NAVIGATION - SEARCH

Authentication dialog workaround for Selenium and Internet Explorer

If you practice test-driven development then you know that unit tests are written to test a very small piece of logic. They are narrow in scope, easy to write and fast to execute. Integration tests, on the other hand, are used to demonstrate that different pieces of a system do work together. Integration tests usually cover entire applications providing affirmation of expected functionality.

For Web applications, the Selenium Web driver can be used to do the automated testing. Selenium is primarily a browser automation software which can be used with NUnit to verify Web application functionality.

If you work in a corporate environment then authentication is often managed by an external security provider such as IBM’s Tivoli Access Manager (TAM) or Microsoft’s NTLM.

In the Windows OS, these authentication technologies generally cause an authentication dialogue to pop up which is not part of the browser. Selenium is only able to automate parts of the page that are accessible via JavaScript. The result of this is the test stalling at the authentication dialogue and eventually timing out which leads to a failing test.

The documented way around this is to encode the credentials as part of the URL like so: http://:/path/to/website. This is fine for Chrome and Firefox but Internet Explorer does not support user names and passwords in the Web site address. This behaviour can be altered via a registry setting. Still, this requires every PC that will potentially execute the tests to have the change applied.

Another option is to use a 3rd party application to automate the Windows GUI. A freeware option is AutoIt (https://www.autoitscript.com/site/autoit/). This solution requires the installation of a 3rd party library which registers a bunch of COM libraries.

Both the above solutions require custom configuration. In an ideal environment, there should be no custom configuration or 3rd party dependencies.

To solve this problem C# with PInvoke can be used to automate the authentication dialogue. In order to automate the dialogue, the Spy++ application available in the Tools folder of your Visual Studio installation can be utilised. Spy++ is a Win32 based utility that gives you a graphical view of the system’s processes, threads, windows and window messages. Using this application it is possible to see the tree structure of windows that have to be traversed in order to get a handle to the corresponding controls that need to be automated. Spy++ works fine for Internet Explorer and to an extent Firefox. Chrome seems to completely render the dialogue using GDI which means there is no window handle available to use in the automation.

The below source code can be used for Internet Explorer. There is also a separate method provided for use with Firefox. Although this feels like a hack, it gets the job done.

using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using HWND = System.IntPtr; 
namespace SeleniumBasedUnitTests
{
    public static class CustomCredentialManager
    {

        private delegate bool EnumWindowsProc(HWND hWnd, int lParam);

        [DllImport("USER32.DLL")]
        private static extern bool EnumWindows(EnumWindowsProc enumFunc, int lParam);

        [DllImport("USER32.DLL")]
        private static extern int GetWindowText(HWND hWnd, StringBuilder lpString, int nMaxCount);

        [DllImport("USER32.DLL")]
        private static extern int GetWindowTextLength(HWND hWnd);

        [DllImport("USER32.DLL")]
        private static extern bool IsWindowVisible(HWND hWnd);

        [DllImport("USER32.DLL")]
        private static extern HWND GetShellWindow();

        [DllImport("user32.dll")]
        private static extern int SendMessage(HWND hWnd, int msg, int Param, string s);

        [DllImport("user32.dll")]
        private static extern HWND GetDlgItem(HWND hDlg, int nIDDlgItem);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern HWND GetWindow(HWND hWnd, GetWindow_Cmd uCmd);
        [DllImport("user32.dll")]
        private static extern HWND SendMessage(HWND hWnd, int msg, HWND wParam, HWND lParam);

        [DllImport("user32.dll")]
        private static extern bool SetForegroundWindow(HWND hWnd);
        private enum GetWindow_Cmd : uint
        {
            GW_HWNDFIRST = 0,
            GW_HWNDLAST = 1,
            GW_HWNDNEXT = 2,
            GW_HWNDPREV = 3,
            GW_OWNER = 4,
            GW_CHILD = 5,
            GW_ENABLEDPOPUP = 6
        }
        public static IDictionary<HWND, string> GetOpenWindows()
        {
            var shellWindow = GetShellWindow();
            var windows = new Dictionary<HWND, string>();

            EnumWindows(delegate(HWND hWnd, int lParam)
            {
                if (hWnd == shellWindow) return true;

                if (!IsWindowVisible(hWnd)) return true;

                var length = GetWindowTextLength(hWnd);
                if (length == 0) return true;

                var builder = new StringBuilder(length);
                GetWindowText(hWnd, builder, length + 1);

                windows[hWnd] = builder.ToString();
                return true;

            }, 0);

            return windows;
        }

        public static void FillInFirefoxCredentials(string userName, string password)
        {
            foreach (var window in GetOpenWindows())
            {
                var handle = window.Key;
                var title = window.Value;

                if (title != "Authentication Required") continue;

                SetForegroundWindow(handle);
                SendKeys.SendWait(userName);
                SendKeys.SendWait("{TAB}");
                SendKeys.SendWait(password);
                SendKeys.SendWait("{TAB}");
                SendKeys.SendWait("{ENTER}");
                SendKeys.Flush();
            }

        }
        public static void FillInIeCredentials(string userName, string password)
        {
            const int wmSettext = 0x000c;
            const int bmClick = 0x00F5;
            var okButtonH = HWND.Zero;

            foreach (var window in GetOpenWindows())
            {
                var handle = window.Key;
                var title = window.Value;

                if (title != "Windows Security") continue;

                handle = GetDlgItem(handle, 0);//1st level window
                handle = GetDlgItem(handle, 0);//second level window
                var i = 0;
                while (true)
                {
                    handle = GetWindow(handle, GetWindow_Cmd.GW_HWNDNEXT);//iterate the list of windows

                    if (i == 1)//OK Button
                    {
                        okButtonH = GetDlgItem(handle, 0);
                    }

                    if (i == 5)//UserName Editbox
                    {
                        var innerH = GetDlgItem(handle, 0);
                        SendMessage(innerH, wmSettext, 0, userName);

                    }

                    if (i == 6)//Password Editbox
                    {
                        var innerH = GetDlgItem(handle, 0);
                        SendMessage(innerH, wmSettext, 0, password);

                        SendMessage(okButtonH, bmClick, HWND.Zero, HWND.Zero);
                        break;
                    }

                    i++;
                }
            }
        }
    }
}

To utilise this class simply use Selenium to navigate to the desired Web URL like so: Driver.Navigate().GoToUrl();

This should then cause the authentication dialogue to pop up.

For Internet Explorer call: CustomCredentialManager.FillInIeCredentials(, );

For Firefox call: CustomCredentialManager.FillInFirefoxCredentials(, );

The result will be the completion of the authentication details and Selenium will continue to the intended URL.

The Internet explorer approach uses handles to manipulate the controls. The Firefox approach uses keyboard automation. Although the keyboard automation method will work on Internet Explorer, it feels like an even bigger hack.

Add comment