Authenticating to Amazon Cognito from Windows Desktop Application

From Aws: “Amazon Cognito provides authentication, authorization, and user management for your web and mobile apps. Your users can sign in directly with a user name and password, or through a third party such as Facebook, Amazon, or Google”.

I am going to assume that you have a basic understanding of what authentication, authorization all the rest means, and that you are actually here because you’ve noticed that Windows Desktop Applications was not included in the list of supported application types.  Also, perhaps you have waded into the AWS documentation and have noticed that User Pool Client Side Authentication Flow is supported by the AWS SDK, BUT only for Android, iOS and JavaScript. If you are a .Net developer and you are either building a new client-server application with RESTful Web API or porting legacy services to the cloud, Identity and Authorization is usually the first challenge. It certainly would be a waste to have to stand up and Windows server just to run ASP.Net Identity to protect some simple Web API. With Amazon Cognito User Pools, the task seems trivial, if only there was an easy way to authenticate users from your client side code. Here’s how.

I encountered this issue while I was working on a project where I was porting an existing Windows based client-server application to the cloud and I really wanted to avoid deploying resources such as as VMs. The requirement was to simply authenticate to a User Pool, retrieve an access token and call a secured server side function. My solution was to use Amazon Cognito for the User Pool and a Lambda function protected by a secured API Gateway method.

This sounds like it should be easy, right? AWS is really just Web Services, how hard could it be to call an authenticate API. There is a promising Cognito API in the AWS .Net SDK , namely AdminInitiateAuth, which accepts username/password credentials to authenticate and retrieve access tokens. Unfortunately, calling this method requires AWS access keys. This is not a problem for a web server running on a AWS EC2 instance because the EC2 instance could inherit the permissions from an instance role. However, if you tried to use this approach from a client side application, you would need to store AWS access keys on the client. That’s NOT a security best practice. So how does the Mobile and Javascript SDKs get around this? Well, the answer lies in the SRP protocol. SRP is a secure password-based authentication and key-exchange protocol. According to AWS User Pool Client Side Authentication Flow, the client must supply SRP details which are constructed with SRP support in the Android, iOS and JavaScript SDKs. If you take the time to inspect the JavaScript SDK you will find that constructing SRP details entails several hundred lines of fairly complex crypto coding.

I really wanted something quick and easy so I was considering popping up a dialog with a web browser control in it and accessing the Cognito pre-built signon page and then scraping access tokens out of the dom, but that seems a bit hacky. Ok, well maybe a lot hacky. After a week of searching for information on getting around the SRP issue, I finally ran across this blog post. Hallelujah! What is this Developer Preview of their Cognito Authentication Extension Library? And why was this so hard to find? Perhaps we’ll never know, but this library did the trick. You can download it directly into your .Net project from Nuget. Just search for  AWSSDK.Extensions.CognitoAuthentication. The documentation here is actually very good and gave me everything I needed to know.

If you would like to see how it all fits together, the rest of this post will cover implementing the whole solution: Authenticating to an Amazon Cognito User Pool and invoking a Lambda function protected by a secured API Gateway method. The basic steps are:

Create a Cognito User Pool

From the AWS Console go to Services->Security, Identity & Compliance->Cognito. Click the Manage User Pools button and the follow the link to create a user pool. Give the user pool the name of Demo  and select Step through Settings. The Attributes page allows you to require specific information regarding users. This will drive the fields on the optional SignUp page that gets generated by Cognito as well as set up some sign-on options. You can also set Password policies on the next page. Next its Multi-factor authentication and account verifications. You can even customize the emails and text messages that will be sent to users for verification purposes. When you get to AppClients, we will need to create a Client for the Windows app, so go ahead and click “Add an app client”. Give the client the “DemoApp”. We can accept the remaining defaults and click “Create app client”. This will generate an App Client Id which will be used for when calling the authentication function from our Windows client-side application. Click Next, and you can specify Lambda functions for various events in the signup and Auth Flow. Click Review and click Create Pool. Once you have created the User Pool, under General Settings, click App Integrations->App client settings and you should see something like the following. Take not of the App client id as we will use this in a later step. Also ensure that the Enabled Identity Providers is set to Cognito User Pool. Finally, you will need to provide a callback url where Cognito will redirect once a user has logged in.

As of the time of this writing, the CallBack URL needs to be an actual we URL, even though the documentation states that is can be something like myapp://. The issue I ran into was that when I set up a new user, that user needed to change their password on first use. As you will see in the next  step, we will use a web browser to access the built-in GUI for this purpose. 

Also note the Allowed OAuth Flows and OAuth Scopes. In order to retrieve the required access token, check Authorization code grant as well as openid.

 

Next, at this time, it would be useful to create a user for your user pool. From the Cognito console under General Settings, click Users and Groups. From there, click Create User. From here, you can create a user and decide whether or not you want that user to validate their identity with an email. For simplicity-sake, just go ahead uncheck Send an Invitation… and hit enter.

If you have the default password policy, you may notice that the FORCE_CHANGE_PASSWORD status is subsequently set. In that case, you will need to enable the built-in UI by providing a Amazon Cognito Domain that will uniquely identify your user-pool. Under App integration, select Domain Name. Choose a unique name and select Check Availability. 

Once you have saved the changes, you can open a web browser and enter your Domain sign-in URL, for example:

https://your_domain/login?response_type=code&client_id=your_app_client_id&redirect_uri=your_callback_url

or in our case:

https://donsdemo.auth.us-east-1.amazoncognito.com/login?response_type=code&client_id=rkealmenpmi7gf69ncnkg0drd&redirect_uri=https://www.cognitodemo.com

Create a Lambda Function and API Gateway Method

Next, we will need to create a Lambda function that will provide the server-side functionality. In the console, go to Services->Compute->Lambda and then Create Function. We will Author from scratch, so simply provide a name. We will use the node.js runtime. For the Role, select Create a new Role. This will take you over to a new page where you will create a lambda basic execution role which will provide the capability to interact with CloudWatch Logs.  The following screen shows the newly created function with initially no triggers and utilizing a single resource, CloudWatch Logs. Next, click Save, to save your unsaved changes. Now, lets add a Trigger. Under Add Triggers, click API Gateway. You will be prompted to pick and existing or add a new trigger. Select Create new API. For Security, select Open. We’ll update that in a bit. Under Additional Settings, note the given name and change the Deployment stage to “Test”, then click Add then Save. At this point, you can test the Gateway API and Lambda function by clicking on the API Endpoint. This should redirect your browser to a page that simply says: “Hello from Lambda”.

 

Secure the API Gateway Method

To secure the Gateway method, in the console select Services->Networking & Content Delivery->API Gateway. From there, select your API Method. Click on the Authorizers link and then Create New Authorizer. Provide and name and for the Type, choose Cognito. You should be able to select your Cognito User Pool from the drop down list. For Token Source select “Authorization”. This is basically telling the API Gateway to set up an Authorizer that will expect an access token in the http header called Authorization. The token is retrieved from the response of your Cognito User’s login request.

Now that you have your Authorizer, click your DemoFunction-API and select Resources. There will be a method underneath titled “Any”. This is basically saying that any REST method (Get, Post, Delete, etc) will be able to be used to access this API. Select Any to view the flow of the method execution. 

Note that on the Method Request, that Auth is set to none. Click the “Method Request” header. Change the Authorization to your Cognito Demo-Authorizer. Don’t forget to click the little checkbox next to the field in order to save the update. Under Request Validator, change that to Validate query string parameters and headers. Then finally, under HTTP Request Headers, add a header called Authorization.

You will need to Redeploy the API in order for the changes to be effective. So under the Actions dropdown, select Deploy API and choose the  Test  stage. Once that is done, the page will show a “Invoke URL” link at the top of the page. Go ahead and try it. This time, the resulting page should read:

{"message":"Missing Authentication Token"}

 

Authenticate from your .Net client-side application

It’s finally time for some client-side programming. Testing this is really easy. I just created a .Net Console application named CognitoDemo in Visual Studio. You will need to add some Nuget packages in order to pull in the required AWS libraries. Here’s the list that is required. If you pull in AWSSDK.CognitoIdentity first, most of the others are dependencies. 

  • AWSSDK.CognitoIdentity
  • AWSSDK.CognitoIdentityProvider
  • AWSSDK.Core
  • AWSSDK.Extensions.CognitoAuthentication
  • AWSSDK.SecurityToken

Here’s the code for the Main entry point:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Amazon.CognitoIdentityProvider;
using Amazon.CognitoIdentityProvider.Model;
using Amazon.Runtime.Internal.Auth;
using Amazon.Runtime;
using Amazon.Extensions.CognitoAuthentication;
using Amazon;
using System.Net;
using System.IO;


namespace CognitoDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var poolId = "<poolId>";
            var clientId = "<clientId>";
            var username = "<userName>";
            var userPassword = "<password>";
            var url = "<API Endpoing URL>";

            //Authenticate and get the access token
            var authFlowResponse = AuthenticateWithSrpAsync(poolId, clientId, username, userPassword).Result;
            var accessToken = authFlowResponse.AuthenticationResult.IdToken;

            //Call AWS API Gateway method 
            var result = CallRestMethod(url, accessToken);

            Console.WriteLine(result);
            Console.ReadKey();
        }

So, we’re simply pulling some values out of the AWS Console. Namely the Cognito Pool Id, the App Client Id, the username and password and the API Gateway Methods Url. The first step is to authenticate and retrieve the access token:

        public static async Task<AuthFlowResponse> AuthenticateWithSrpAsync(string poolID, string clientID, string username, string userPassword)
        {
            try
            {
                var region = RegionEndpoint.GetBySystemName("us-east-1");

                var provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), region);
                // FallbackRegionFactory.GetRegionEndpoint());
                CognitoUserPool userPool = new CognitoUserPool(poolID, clientID, provider);
                CognitoUser user = new CognitoUser(username, clientID, userPool, provider);

                string password = userPassword;

                AuthFlowResponse context = await user.StartWithSrpAuthAsync(new InitiateSrpAuthRequest()
                {
                    Password = password
                }).ConfigureAwait(false);

                return context;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

We create a AmazonCognitoIdentityProviderClient and a CognitoUserPool. Next we create the CognitoUser and initiate an AuthRequest, passing in the password. If all goes well, we get back an AuthFlowResponse with an IdToken. We can use the token in the HttpWebRequest as shown below. Note we add the header “Authorization” matching what we specified when we were assigning the Authorizer to the WebApi previously.

       public static string CallRestMethod(string url, string accessToken)
        {
            try
            {
                HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(url);
                webrequest.Method = "GET";
                webrequest.ContentType = "application/x-www-form-urlencoded";
                webrequest.Headers.Add("Authorization", accessToken);
                HttpWebResponse webresponse = (HttpWebResponse)webrequest.GetResponse();
                Encoding enc = System.Text.Encoding.GetEncoding("utf-8");
                StreamReader responseStream = new StreamReader(webresponse.GetResponseStream(), enc);
                string result = string.Empty;
                result = responseStream.ReadToEnd();
                webresponse.Close();
                return result;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

And there you have it. We’ve constructed an AWS Cognito User Pool and populated it with users. We’re developed some serverless web API using API Gateway and Lambda. We then protected that API with a Cognito Authorizer. Now that we have this basic flow down, we would also use federated identity providers such as LDAP and even web providers like Facebook or Google.

I hope you enjoyed reading this post. I’m really excited about using this capability in several projects that require me to move legacy applications into the cloud. As always, let me know what you think and let me know if you have come up with better or more interesting ways to solve the same problem. Cheers!

 

About the Author Don McRae

CEO of McRaeSoft.com and Independent Software Development Consultant. Specializing in AWS Cloud Architecture, Development and DevOps and Cloud Security.

Leave a Comment: