The chicken can dance

Rubber Chicken Paradise

Making the chicken dance one line of code at a time

AWS Cognito with Winforms

Lets make the legacy app work with the new stuff

Jeremy Oursler

5 minutes read

Legacy software is fun. Sometimes we need to update code to enable new authentictaion methods. In todays adventure our intrepid developer heads off on the quest to get the legacy .net framework 4.6.1 WinForms app working with the new user store, AWS Cognito.

AWS Cognito Setup

So after we get our user pool set up we can create an app client for our application. This is the config that I am working with for this post.

Discovery

Since WinForms isn’t a browser we can’t just use the built in window.location to send our browser off to the hosted login page and listen for the redirect. Instead we need some way to open a browser to the login page and then listen to the redirect.

After looking around for a good tutorial on AWS Cognito and WinForms and coming up blank I went to using a generic OpenId Connect client. The great thing about Cognito is its standards compliant.

Heading off to the OpenID Connect certified libraries there is only one C# library listed (Makes things easy), IdentityModel.OidcClient.

Attempt One

Looking through the samples at IdentityModel.OidcClient.Samples there is a WinForms example using the WebViewer control. Since I don’t get paid to reinvent the wheel I copied in WinFormsWebView.cs and ExtendedBrowser.cs.

Since I wanted to have the login run on application load I added the config below and in the form_load I call _oidcClient.LoginAsync()

public Form1()
{
    InitializeComponent();

    var options = new OidcClientOptions
    {
        //This is the url before https://cognito-idp.{AwsRegion}.amazonaws.com/{AwsUserPool}/.well-known/openid-configuration
        Authority = "https://cognito-idp.{AwsRegion}.amazonaws.com/{AwsUserPool}",
        ClientId = Config.ClientId,
        //Any scopes you want
        Scope = "openid email",
        RedirectUri = "http://localhost/winforms.client",

        Browser = new WinFormsWebView()
    };

    _oidcClient = new OidcClient(options);
}

public Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) 
{
    var loginResult = await _oidcClient.LoginAsync()
    //Handle Login result
}

Let’s hit start and off we go. The WebViewer control comes up and everything is looking great until the JavaScript errors start. Turns out the default IE version for the web viewer control is IE 7 which is not supported by AWS Cognito (I don’t really blame them, I don’t want to support IE 7 either). There are some registry hacks that can make it work but modifying the registry is something I don’t want to have to do since that requires elevated permissions.

Implementing the code from the stack overflow post did work and solve the issue but having to give the users admin on their local machine was a no go.

Attempt number two

Looking through the IdentityModel.OidcClient I noticed the ConsoleClientWithBrowser example and the SystemBrowser.cs. This uses a self hosted Kestrel HTTP server on a port to listen for the redirect URL. Since AWS Cognito can’t have a dynamic port (well my * attempt failed so just went with a static port) I picked a random port number (fairly chosen by dice roll, guaranteed to be random), cross my fingers and hope nothing else is running on their systems on that port (end user systems so the magic 8 ball says “Outlook Promising”).

public Form1()
{
    InitializeComponent();

    var browser = new SystemBrowser(9999);

    var options = new OidcClientOptions
    {
        //This is the url before https://cognito-idp.{AwsRegion}.amazonaws.com/{AwsUserPool}/.well-known/openid-configuration
        Authority = "https://cognito-idp.{AwsRegion}.amazonaws.com/{AwsUserPool}",
        ClientId = Config.ClientId,
        //Any scopes you want
        Scope = "openid email",
        RedirectUri = $"http://localhost:{browser.Port}",

        Browser = browser
    };

    _oidcClient = new OidcClient(options);
}

public Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) 
{
    var loginResult = await _oidcClient.LoginAsync()
    //Handle Login result
}

After making the necessary change to the Cognito App Client config for the callback url we hit start and success. The WinForms app starts up, the default system browser launches (Chrome in my case), the hosted login page does its thing, redirects to our Kestrel HTTP Server which the OidcClient is listening to and we have our token information.

Notes on the config

If you are using a custom domain name with AWS Cognito you will get an error because the publisher and auth url do match the authority. Adding options.Policy.Discovery.ValidateIssuerName = false ; and options.Policy.Discovery.ValidateEndpoints = false; after initializing the options object but before newing the OidcClient will fix the issues.

What do now?

After we have our token the legacy app loads a bunch of user profile data and permissions out of a couple databases. I was able to replace the login screen and get the various pieces of data from the API where available currently and the database where still using the legacy connections.

Overall it was quicker than I thought since Cognito is OIDC standards compliant which helps with just about anything related to auth.

Post Script

Since the process launches a system browser window, sometimes the WinForms app window can get covered up. The below utility class will pull the window back to the foreground after the login processing has completed.

Rcp.Utilities.WindowHelpers is a quick class to bring the current process to the foreground using the Win32 APIs

    public class WindowHelpers
    {
        private static class User32
        {
            [DllImport("User32.dll")]
            internal static extern IntPtr SetForegroundWindow(IntPtr hWnd);

            [DllImport("user32.dll")]
            internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

            internal static readonly IntPtr InvalidHandleValue = IntPtr.Zero;
            internal const int SW_SHOWNORMAL=1;
        }
        public static void Activate()
        {
            Process currentProcess = Process.GetCurrentProcess();
            IntPtr hWnd = currentProcess.MainWindowHandle;
            if (hWnd != User32.InvalidHandleValue)
            {
                User32.SetForegroundWindow(hWnd);
                User32.ShowWindow(hWnd, User32.SW_SHOWNORMAL);
            }
        }
    }

Rcp.Utilities.WindowHelpers

Recent posts

See more

Categories

About

An ADHD programmer writing about the random stuff they run into. Check the about page for more.