Amazon Cognito - Function over form

A backend developer's experience in adding user authentication to his backend application.

Page content

Many individuals, builders or backend developers, often grapple with the challenge of balancing function and form. Ultimately, the success of our application hinges on ensuring that the Minimum Viable Product (MVP), Proof of Concept (PoC), or prototype functions smoothly before investing substantial resources into a flashy frontend.

However, there lies a conundrum – how do we control access to our application without prematurely exposing it to the entire internet?

Image

Why am I sharing this journey with you? During the holidays, I ventured into the realm of building a new project. While delving into the backend intricacies involving services like API Gateway, Lambda, and a plethora of DynamoDB databases, one unavoidable aspect surfaced – the need for a frontend. Even if it’s just a basic HTML page, having a visual representation of the backend processes is crucial.

In this blog post, I aim to share my experiences with AWS Cognito. It’s been a five-year mission, boldly going where many have gone before, yet I’ve always found myself struggling as a BUILDER.

By the end of this we’ll step through what I have learned and try to provide some code examples for you to take away and use yourself.

Introducing Amazon Cognito

“With Amazon Cognito, you can add user sign-up and sign-in features and control access to your web and mobile applications. Amazon Cognito provides an identity store that scales to millions of users, supports social and enterprise identity federation, and offers advanced security features to protect your consumers and business. Built on open identity standards, Amazon Cognito supports various compliance regulations and integrates with frontend and backend development resources.” - https://aws.amazon.com/cognito/

In my past life as a PHP Developer I’d write web applications with a specific business function. I’d create some level of users table, with an encrypted password and some questionable business logic as to how to access this table. I’d be left with having to maintain the DB, the table, the code and the server to ensure that user authentication was safe and secure.

“Did someone order a portion of operational overhead with a side of operational headache?”

Amazon Cognito is designed to take away the hassle of looking after authentication methods, userpools or even federation of users. A serverless, managed service that also has a very generous feature set and free tier. Yep… FREE! For up to 50,000 Monthly active users or 50 Monthly federated users (Federated infers social logins such as Azure AD, Google, Facebook, Apple and Amazon). It provides an authentication mechanism for your application which can then also provide IAM privileges to resources in your infrastructure such as API Gateway, DynamoDB et al.

The Problem

I’ll admit to trying my hand at Cognito several times over and getting basic knowledge of how it works but never actually getting something that is working to a level of productivity. I always ended up in the past with some random token or some obfuscated URL and then a severe lack of time to troubleshoot / educate myself on how to properly use these elements.

However in this example, I had a WHOLE festive holiday to work this out once and for all, and it really wasn’t that complicated once I got to the bottom of it.

The Theory

Yes there’s a boring theory section to this but it’ll make sense eventually.

Workflow

In order to successfully implement Cognito you’ll need a user form, to capture basic information (username and password), you send an InitiateAuth call, which sends back a series of challenge and response messages (SMS MFA,etc) to then eventually be sent an array of tokens to be used in authentication against others AWS services.

Image

Source - (https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html)

The key portion of this for our example is the return of the Access, ID and refresh token. This then allows us to implement the architecture that we’ll be more familiar with, below;

Image

Let’s look at the definition of each token, based on the AWS Docs:

ID token

“The ID token is a JSON Web Token (JWT) that contains claims about the identity of the authenticated user, such as name, email, and phone_number. You can use this identity information inside your application. The ID token can also be used to authenticate users to your resource servers or server applications.” - https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html

Access token

“The user pool access token contains claims about the authenticated user, a list of the user’s groups, and a list of scopes. The purpose of the access token is to authorize API operations. Your user pool accepts access tokens to authorize user self-service operations. For example, you can use the access token to grant your user access to add, change, or delete user attributes.” - https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html

Refresh token

“You can use the refresh token to retrieve new ID and access tokens. By default, the refresh token expires 30 days after your application user signs into your user pool. When you create an application for your user pool, you can set the application’s refresh token expiration to any value between 60 minutes and 10 years.” - https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-refresh-token.html

The Implementation

In practice this now starts to make more sense, now we understand what the different tokens are used for.

Prerequisites

Before following the below, make sure you’ve done the below;

  • Grab a copy of the supporting Github repo (https://github.com/alanblockley/cognito-javascript-example)
  • Setup an instance of Cognito in your AWS account. A Cloudformation template has been provided for you in the attached Github repo.
  • Enter your pool id and client id into the config.js, supplied in the repo. The config.js file should go in the same location as the js file calling it.

Now this is done, let’s get started.

Registration

Every good authentication method needs a way of registering users. How else are your users going to sign up?

Let’s give it an interface!

<form id="registerForm" novalidate onsubmit="return false">
    <div class="form-group">
        <label for="emailInputRegister">Username</label>
        <input type="text" class="form-control" id="emailInputRegister" placeholder="Enter email">
    </div>

    <div class="form-group">
        <label for="passwordInputRegister">Password</label>
        <input type="password" class="form-control" id="passwordInputRegister" placeholder="Enter Password">
    </div>

    <div class="form-group">
        <label for="confirmationPassword">Password</label>
        <input type="password" class="form-control" id="confirmationPassword" placeholder="Confirm Password">
    </div>

    <button type="submit" class="btn btn-primary btn-block">Register</button>
</form>

...

<script src="js/register.js"></script>
<script type="text/JavaScript">
    $('#registerForm').on("submit", function (event) {
      registerUser();
      event.preventDefault();
    })
</script>

[ https://github.com/alanblockley/cognito-javascript-example/blob/main/html/register.html ]

Nothing too clever here. Just accepting an email address and a password, plus a confirmation that the password was spelt correctly.

In the <script> section of the HTML file we’re including the register.js file and then defining a trigger. This can be done in the one file normally but I like to keep triggers in line for visibility.

It’s worth noting that we also need to call a couple of other AWS specific scripts just above our script section;

<script src="assets/js/config.js"></script>
<script src="js/vendor/aws-cognito-sdk.min.js"></script>
<script src="js/vendor/amazon-cognito-identity.min.js"></script>

These two reference libraries that allow us to call Amazon Cognito via the SDK, natively in Javascript, the other is our configuration file for all Amazon Cognito calling scripts.

This trigger referenced calls the below function;

// Function to register a user
function registerUser() {
   // Get user input values from the registration form
   var email = document.getElementById("emailInputRegister").value;
   var password = document.getElementById("passwordInputRegister").value;
   var confirmationPassword = document.getElementById("confirmationPassword").value;
  
   // Check if the entered passwords match
   if (password != confirmationPassword) {
       // Display an alert if passwords do not match
       alert("Passwords do not match");
       return;
   }


   // Display an alert with user registration information
   alert("Registering user with email: " + email + " and password: " + password);


   // Configuration data for Cognito user pool
   poolData = {
       UserPoolId : _config.cognito.userPoolId,
       ClientId : _config.cognito.userPoolClientId
   }


   // Create a new Cognito user pool object
   var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
   var attributeList = [];


   // Create an attribute for the user's email
   var dataEmail = {
       Name : 'email',
       Value : email
   }


   var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);


   // Add the email attribute to the attribute list
   attributeList.push(attributeEmail);


   // Sign up the user with Cognito user pool
   userPool.signUp(email.replace("@", "_"), password, attributeList, null, function(err, result){
       if (err) {
           // Display an alert if there's an error during user registration
           alert(err.message || JSON.stringify(err));
           return;
       }
       // User registration successful
       cognitoUser = result.user;
       alert('User registered successfully');


       // Display a success message to the user
       document.getElementById("successMsg").innerHTML = "Check email for verification code";
       $('#successMsg').show();
   });
}

[ https://github.com/alanblockley/cognito-javascript-example/blob/main/js/register.js ]

The JavaScript you’ve got here is all about making user registration smooth using Amazon Cognito. It kicks off by grabbing what the user typed in for email, password, and the confirmation.

Quick check to see if the passwords match, and if not, it throws a friendly alert. Assuming all’s good, it sets up a user using the Amazon Cognito service, putting together some pre-configured info. The user’s email is added as an attribute, and then it’s signup time with Cognito. If all goes well, a success message pops up, telling the user to check their email for a verification code.

It relies on the Amazon Cognito Identity SDK to handle the heavy lifting and brings together user registration, password checks, and Cognito integration for a seamless experience on a web app.

Login

Using a simple login.html we’re able to now send basic username and password information, registered in our previous example, to Cognito and receive a token.

Again, we will now set up an interface to login. It’s not pretty, but remember, we’re not front end developers. Function over form!

<form id="signinForm" novalidate onsubmit="return false">
  <div class="form-group">
      <label for="emailInputSignin">Username</label>
      <input type="text" class="form-control" id="emailInputSignin" placeholder="Enter email">
  </div>
  <div class="form-group">
      <label for="passwordInputSignin">Password</label>
      <input type="password" class="form-control" id="passwordInputSignin" placeholder="Password">
  </div>

  <button type="submit" class="btn btn-primary btn-block">Login</button>
</form>

...

<script src="js/vendor/login.js"></script>

<script type="text/JavaScript">
  $('#signinForm').on("submit", function (event) {
    userLogin();
    event.preventDefault();
  })
</script>

[ https://github.com/alanblockley/cognito-javascript-example/blob/main/html/login.html ]

And don’t forget our libraries and config.

<script src="assets/js/config.js"></script>
<script src="js/vendor/aws-cognito-sdk.min.js"></script>
<script src="js/vendor/amazon-cognito-identity.min.js"></script>

This trigger in our not so pretty login page calls the below function;

// Function to handle user login with local storage
function userLogin() {
    
    // See if the id_token already exists in session storage
    let id_token = sessionStorage.getItem("id_token");

    if (id_token) {
        // If id_token exists, user is already logged in
        alert("Already logged in");
    } else {
        // Get user input values for email and password from the signin form
        var email = document.getElementById("emailInputSignin").value;
        var password = document.getElementById("passwordInputSignin").value;

        // Configuration data for Cognito user pool
        poolData = {
            UserPoolId : _config.cognito.userPoolId,
            ClientId : _config.cognito.userPoolClientId
        }

        // Create a new Cognito user pool object
        var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
        var userData = {
            Username : email,
            Pool : userPool
        }

        // Create a new Cognito user object
        var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

        // Authenticate the user with Cognito using provided credentials
        cognitoUser.authenticateUser(new AmazonCognitoIdentity.AuthenticationDetails({
            Username : email,
            Password : password
        }), {
            onSuccess: function (result) {
                // If authentication is successful
                console.log('Successfully logged in');

                // Get and store access, ID, and refresh tokens in session storage
                var access_token = result.getAccessToken().getJwtToken();
                var id_token = result.getIdToken().getJwtToken();
                var refresh_token = result.refreshToken.token;

                sessionStorage.setItem("id_token", id_token);
                sessionStorage.setItem("access_token", access_token);
                sessionStorage.setItem("refresh_token", refresh_token);

                // Display success message to the user
                alert("Successfully logged in");
                
                // Additional actions or navigation can be added here
            },
            onFailure: function(err) {
                // If authentication fails, display an error message
                alert(err.message || JSON.stringify(err));
                return;
            }
        });
    }
}

This piece of JavaScript is all about handling user logins, and it’s got a clever twist with local storage. It first checks if there’s an existing ID token stashed in the session storage. If it finds one, it means the user is already logged in, and a friendly “already logged in” alert pops up. But if not, it goes on to grab the user’s email and password from the signin form.

Then, it sets up the user pool with the familiar Cognito data and creates a new Cognito user using the provided credentials. When it tries to authenticate the user, if successful, it logs a victory message, snags the access, ID, and refresh tokens, and stores them in the session storage.

A triumphant “Successfully logged in” alert signals the completion. You can practically smell the success from here!

Once we have these two methods implemented in practice, we log in, see our wonderful app and we can carry on developing. One of the gotchas you’ll find is that after about an hour, your app will start giving you 401 errors in the development console.

This is because your access and ID token have expired. So what do we do now?

This is where the refresh token comes in!

Refresh token

One of the tricks in your code has to be to acknowledge when your resources start returning HTTP 401 errors. This can stop the whole show if not dealt with.

It’s up to you and your logic how you detect 401 errors but below is an example Javascript function to refresh the token to then keep things moving.

// Function to refresh the Amazon Cognito authentication token
function refreshToken() {

    // Retrieve the existing id_token and refresh_token from session storage
    let id_token = sessionStorage.getItem("id_token");
    let refresh_token = sessionStorage.getItem("refresh_token");

    // Check if id_token exists
    if (id_token) {
        // Configuration data for Cognito user pool
        poolData = {
            UserPoolId : _config.cognito.userPoolId,
            ClientId : _config.cognito.userPoolClientId
        }

        // Create a new Cognito user pool object
        var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
        var userData = {
            Username : id_token, // Using id_token as the username for refreshing
            Pool : userPool
        }

        // Create a new Cognito user object
        var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
        
        // Create a CognitoRefreshToken object using the refresh_token
        var cognitoRefreshToken = new AmazonCognitoIdentity.CognitoRefreshToken({RefreshToken: refresh_token});

        // Refresh the session using the refresh token
        cognitoUser.refreshSession(cognitoRefreshToken, (err, session) => {
            if (err) {
                // Log any errors during token refresh
                console.log(err);
                return;
            }

            // Get and update the new id_token and access_token
            id_token = session.getIdToken().getJwtToken();
            access_token = session.getAccessToken().getJwtToken();

            // Store the new tokens in session storage
            sessionStorage.setItem("id_token", id_token);
            sessionStorage.setItem("access_token", access_token);

            // Now that we have new tokens, additional actions can be performed
            // ...

            // Display a success message to the user
            alert('Auth token refreshed');
        });
    }
}

The final JavaScript function, refreshToken, is designed to handle the refreshing of Amazon Cognito authentication tokens, specifically the id_token and access_token. It begins by retrieving the existing id_token and refresh_token from the session storage.

The function checks if an id_token exists and proceeds to configure the Cognito user pool with predefined pool data. It then creates a Cognito user object using the id_token as the username for refreshing. A CognitoRefreshToken object is created using the existing refresh_token. The function then attempts to refresh the session using the cognitoRefreshToken.

If successful, it updates the id_token and access_token with the new tokens obtained from the refreshed session and stores them back in the session storage. Additionally, the function provides a placeholder comment indicating that additional actions can be performed with the new tokens.

Finally, a success message is displayed to the user via an alert. In summary, this function ensures that the user’s authentication tokens stay up-to-date by refreshing them when needed, contributing to a more seamless and secure user experience in applications using Amazon Cognito.

Sources