All Articles

Understanding Amazon Cognito

As a developer, one of the more baffling AWS offerings has been Amazon Cognito. Authentication is usually already implemented inside the applications I work on, so I’ve often wondered what is the point of this service if I’m not building something from scratch.

Even then, what Cognito offers has been difficult to understand without building an application with it. It provides functionality that is relevant to new applications, all the way up to those with extensive authentication processes already included.

This post is an attempt to provide an overview of its services, having applied them to both an existing application that has authentication, and a new one.

Three Services

Like many AWS services, Cognito is an umbrella of loosely related services:

  • Cognito User Pools
  • Cognito Identity Pools
  • Cognito Sync

Cognito User Pools is at its heart a user directory. It stores credential information about your users, such as their username and password or Single Sign-On (SSO) information for a third-party authentication provider such as Google, Facebook or Apple. You can delegate all or part of your authentication requirements to it.

Cognito Identity Pools is a more specific service that provides federated identities for use with AWS services. It allows you to take an authenticated (or unauthenticated) user from another system swap their credentials for temporary AWS permissions they can use to access AWS services in your account (more on why you want this later on).

Cognito Sync is a service for cross-device sync of user data across devices. It has mostly been supplanted by AWS AppSync, and recommendation is to use that now.

(I won’t go into any futher detail about Cognito Sync in this post)

Cognito User Pools

Authentication is a common concern to most applications, but it is both critical and complex, making it important but difficult to get right. Cognito User Pools takes care of most of your authentication concerns, removing much of the ‘undifferentiated heavy lifting’ from your application development process.

When implementing authentication, there are several things you may need to worry about. Cognito User Pools can handle most or all of the following for you (depending on how much customisation you want to provide):

  • Capturing and storing user credentials safely
  • Providing a standard flow for users to sign-up, login and reset their password
  • Confirming a user’s email address or mobile phone number
  • Allowing a user to secure their account with Multi-Factor Authentication
  • Linking a user identity to a role or group
  • Providing a user interface for login, password reset and sign-up
  • Allowing users to login with popular third-party SSO providers or via SAML
  • Performing actions in your database or back-end in response to user-related events, like after sign-up, after confirmation, after sign-in, etc.

At the very least, Cognito User Pools can be configured to store your users and their related credentials.

You can then implement login and signup in your own app, or you can choose to use its hosted UI, which provides standard pages and flows for you. These can be customised with your own themes and CSS changes and hosted on a custom domain name.

You integrate Cognito with your application either with direct API calls to issue tokens, or with callback URLs (both as a part of standard OAuth flows). Several options for authentication flows are available, which differ depending on your application (Mobile vs Web, Admin-only flows, migrating from an existing system, etc.)

It also can take care of sending confirmation and password reset emails, which you can trigger from your code or via its hosted UI. Emails are send via SES and can be customisd.

Login options include conventional username password, or with a third-party identity provider like Google, Facebook, Apple or Amazon.

Additionally, you can connect it to a SAML Identity Provider if you want to federate your app with an existing user directory. OIDC Identity Providers can also be connected for authentication.

Cognito User Pools issues standard OIDC tokens (a type of JWT token used in OAuth2 environments), which you can validate in your application and to authorise your user’s access.

Many AWS services have built-in support for Cognito User Pool tokens (like API Gateway and AppSync), which means you do not need to write your own JWT validation code.

User Pools also support groups, which are collections users can be added to for rudimentary authorisation purposes. They can be associated with IAM roles, so that you your users can access AWS resources directly, with secure, fine-grained permissions.

Groups also appear in your tokens as extra claims, and can be used for simple in-app authorisation purposes, such as restricting access to functions based on permissions.

Cognito User Pools also provides a console for managing users and groups, which makes it easier for an administrator to support with developing a dedicated console.

Lastly, you can add your own customisation to the authentication, signup and confirmation flows by registering AWS Lambda triggers which run on user events, such as:

  • before signup (e.g. accept or deny a request for signup with custom validation)
  • after confirmation (e.g. user clicking a link in a confirmation email)
  • before authentication (e.g. provide additional validation on an authentication request)
  • after authentication
  • pre-token generation (e.g. to add your own claims to the token)
  • on user migration (e.g. to perform extra work on a user logging for the first time from a migrated system)
  • custom message (e.g. customise MFA or post-confirmation messages)

Cognito Identity Pools

Identity Pools let you exchange existing user credentials for AWS permissions. Each user gets their own identity in the pool. It is exchanged for temporary AWS credentials (essentially a time-restricted access key and session token) tied to a specific role that you specify.

Identity Pools fills a gap where you want to provide direct access from an application to specific AWS services, but you don’t want to or can’t use Cognito User Pools to store and manage your users.

Common services used directly by users via a access control include:

  • Amazon API Gateway to invoke APIs
  • AppSync to call a GraphQL API
  • Direct DynamoDB table access
  • Upload objects to S3 (without pre-signed URLs)
  • Write messages to a Kinesis Stream

Traditionally you would put your own authentication process in front of these APIs via a REST API or similar, but this can be cumbersome, especially with newer AWS services like AppSync which are designed to be accessed directly. By issuing credentials with a specific role to your user, you can delegate the authentication to IAM and handle the authorisation both with the given IAM role and within the specific service.

Your users can come from:

  • Cognito User Pools
  • An external identity provider (such as SAML or a social identity provider like Facebook)
  • Users authenticated an existing application

In my case, I had an existing system with its own authentication and roles, but I wanted to use AWS AppSync to provide a GraphQL API to my users. AppSync can authorise this API with a Cognito User Pools token, but in my case I did not want to migrate to Cognito User Pools.

Instead, I was able to provide an authenticated API in my application that took an existing user and used Amazon Cognito Identity Pools to create an identity for them and issue them with a JWT token which could be exchanged for temporary IAM credentials.

Use Cases

These credentials were scoped to access the AppSync GraphQL endpoint I wanted to provide access to. It is then the responsibility of the downstream service to provide authorisation (e.g. using existing application roles and groups), which in the case of AppSync, was done in my Lambda Data Sources.

Another common use case is to use an identity pool to authorise access to a API Gateway. As mentioned previously, Cognito User Pools are already supported directly as an API Gateway Authorizer (along with their control of IAM roles via groups), but if you have your own authentication process and and want control access instead with an with an Identity Pool, you can configure your API Gateway with IAM authentication, and then restrict access to the individual REST endpoints with the role you pass from the identity pool.

Controlling access to resources by user

You can customise the IAM role you pass to the user. IAM allows very fine-grained access to resources, and Cognito Identity Pools provides Condition keys that let you restrict access to specific identity pools and even specific identities.

If you use Cognito User Pools, SAML or a third-party provider you have several options to control the IAM role issued to the user, such as rule-based mappings or via rules on the token claims. This makes it easier to control the IAM role issued to a user based on something in the token or SAML assertions, such as an application or organisation specific role.

When using a developer provided identity, one limitation to note however, is that an Identity Pool is restricted to an 'authenticated' and 'unauthenticated' role for the pool. This means you cannot easily customise the IAM role you pass to specific users. Instead, more granular resource access (e.g. resources in a DynamoDB table or fields in an AppSync endpoint) may have to be controlled within a mechnanism in the service itself or with very carefully designed IAM condition keys.

For example, to control access to DynamoDB rows, you may need to key on the identity ID (issued by the pool to a user) in your DynamoDB item and then use IAM condition keys (see this guide for examples).

In AppSync, you can do the same with your resolver templates by matching on the identity ID against the data being returned, or if you are using Lambda Data Sources, by passing the identity information via the resolver request mapping template to allow it to perform authorisation.

Implementing Cognito in your app

In a future post, I’ll go into more detail about these services and how they are added to your app.