AWS Cognito – An Implementation Guide for your NodeJS Application

In this article, we aim to give you an overview of what AWS Cognito solves and how to use it as your app’s authentication provider, as well as explain how to use the concepts of Id, Access, and Refresh Tokens. We will also explain a problem we worked on and take a look at the library used (amazon-cognito-identity-js).

What is Cognito and What does it Solve?

Cognito is a user management service by Amazon Web Services [1]. It is a very interesting option for those who wish to quickly focus on coding your app instead of having to set up the boring sign-up/authentication flow and all that comes with user management, like password recovery. Besides it, Cognito is free to up to 50 000 active users, which is great for starting businesses. It works by grouping users the way your business need, giving them roles, enforcing minimum password strength, two-factor authentication (2FA), and finally returning three tokens (two of them are the well-known JWT). Later, it provides you a user database as well as tools to manage them.

Despite the default password recovery mechanism being e-mail, if you want to use password recovery by SMS, you’ll just have to hire the AWS Simple Notification Service (SNS), but that’s a topic to another article.

The Problem

Recently, we’ve received the challenge to model and develop from scratch an application that only required authentication for users that were registered by a separate admin platform. So, to focus on what matters, we decided to go with Cognito and save that precious time and effort.

Speaking in general, this is the flow we wanted. A central server providing both authentication and sign-up for new users. The app would not have an option to register new users, and should not know we are using Cognito as our authentication service. All it knows is that the authentication system will return and take one to three tokens to operate.

The first approach was using the SDK itself, but we find it to not be very clear for a first encounter, so we thought that there could be a higher-level solution that could abstract a few flows and present the service in a friendlier way.

Looking at NPM, we went for the most downloaded package since it covered almost all we wanted: amazon-cognito-identity-js [2]. The problem was it was designed for an approach that would require the app to know Cognito and request from him directly, holding the auth tokens in its memory, and having the AWS credentials inside its environment (as exemplified in the image below).

The package (or library) we chose implements lots of amenities shown in Cognito docs, like handling “first access” (they have a special rule for this case, forcing you to change your admin-given or automatically-generated password, handling flows that throw data from here to there). This is why we decided to use this ready-to-go solution instead of implementing it all for ourselves directly from the AWS-SDK; save time and go to an easier implementation.

This default flow was not fully interesting for us, however, because we would have to give AWS credentials for the entire App, Admin, and Server. It would break a few safety precautions and duplicate the implementation. That being the case, we decided to implement the package server-side and kind of act like a proxy to Cognito from the App and Admin perspectives, isolating the knowledge of which authentication provider we are using.

The Tokens

Before going to the solutions it would be important to talk a little about the Three Tokens first:

  • Id Token: A JWT token, which when decoded will provide information that identifies its bearer. Information like email, name, and phone number.
  • Access Token: A JWT token which when decoded will provide information about its bearer access. Information like groups it belongs to.

Ideally, both Id and Access will be valid just for a few minutes to a couple of hours. If one of them leaks, the bearer can authenticate until their validity. They cannot be revoked. All you can do is make a list of leaked tokens and check each call if they bear a listed token.

  • Refresh Token: The long-life token. Not a JWT and it’s used to obtain new Id and Access Tokens. Meant to expire in any duration from a few minutes to many days. This is probably the most sensitive token and the only one easily revoked. Its bearer can keep their authentication, emitting new access tokens whenever they wish to.

All these tokens can have their duration configured in the Amazon Web Services dashboard.

The Solution

A decision in design we made was that the Server should not store any App session data because it doesn’t have to directly know who’s using the application at the moment. The Cognito service will be the only one responsible for managing sessions. This package, meant to be used client-side, by default stores in-memory the three session tokens. Because of that, the Server got to be careful when processing auth-requiring operations like password change (not password recovery). Depending on which operation the App is requesting, it’ll have to send all three tokens (ID Token, Access Token, and Refresh Token [3]) to create a local session and then do what it wants to do.

A good example is the "Use Case 11" presented at the library’s README [2]: "Changing the current password for an authenticated user". As we can see, it doesn’t receive any information about what user is changing its password, so it expects to have the tokens registered in its internals. The same occurs to revoke the issued tokens, exemplified by "Use Case 15" ("Global sign out"). The cognitoUser instance is set at "Use Case 1", by the way. We will set it manually.

Use Case 11:

cognitoUser.changePassword('oldPassword', 'newPassword', function(err, result) {
    if (err) {
        alert(err.message || JSON.stringify(err));
        return;
    }
    console.log('call result: ' + result);
});

Use Case 15:

cognitoUser.globalSignOut(callback);

To set the session manually, we had to find a way to input this information without signing the user in again, because at every sign in we do, new tokens are emitted and we don’t want that. We want tokens to be used until the end of their lifespans, and revoke the Refresh Token if we do not want this session to be valid anymore.

Browsing the code, we found there were a few entities of interest: User (we just saw that), Pool, Session, IdToken, SessionToken, and RefreshToken. The Session has three tokens; The User has a Pool, a username, and (implicitly) a Session.

Like this:

const AmazonCognitoIdentity = require('amazon-cognito-identity-js');

const POOL_DATA = {
  UserPoolId: process.env.POOL_ID,
  ClientId: process.env.CLIENT_ID
};

class AuthenticatedOperations {
  constructor({ username, idToken, accessToken, refreshToken }) {
    this.username = username;
    this.pool = new AmazonCognitoIdentity.CognitoUserPool(POOL_DATA);
    this.user = new AmazonCognitoIdentity.CognitoUser(
      { Username: username, Pool: this.pool }
    );
    this.session = new AmazonCognitoIdentity.CognitoUserSession({
      IdToken: new AmazonCognitoIdentity.CognitoIdToken({ IdToken: idToken }),
      AccessToken: new AmazonCognitoIdentity.CognitoAccessToken({ AccessToken: accessToken }),
      RefreshToken: new AmazonCognitoIdentity.CognitoRefreshToken({ RefreshToken: refreshToken })
    });
  }

  signOut() {
    this.user.setSignInUserSession(this.session);
    return new Promise((resolve, reject) => {
      this.user.globalSignOut({
        onSuccess: resolve,
        onFailure: reject
      });
    });
  }

async changePassword({ currentPassword, newPassword }) {
    this.user.setSignInUserSession(session);
    return new Promise((resolve, reject) => {
      this.user.changePassword(currentPassword, newPassword, (err, result) => {
        if (err) {
          return reject(err);
        }
        return resolve(result);
      });
    });
  }
}

The above snippet is enough to give an insight into the entities the library uses and how they relate with each other. Notice that, at the beginning of each method the Session is set by a User method, and just then the User action is called. Notice also that the constructor is responsible for building the User and the Session.

Conclusion

This took a while for us to figure it out since we did not find any instruction of usage as we are using it here. We had to crawl inside the amazon-cognito-identity-js library to figure out the classes and methods we should use to make it work server-side. For admin methods, this library does not offer resources enough, so it’ll be needed to use the SDK [4] itself. Methods like List Users, edit users and so are only implemented with that.

Also, take a look at the example I wrote on GitHub. https://github.com/dougmrqs/aws-cognito-identity-js-example

[1] https://aws.amazon.com/pt/cognito/

[2] https://www.npmjs.com/package/amazon-cognito-identity-js

[3] https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html

[4] https://www.npmjs.com/package/aws-sdk

[4.1] https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentity.html

We want to work with you. Check out our "What We Do" section!