Configuring Two Cognito User Pools for a Single AppSync GraphQL API: A YAML CloudFormation Template Guide

2022-11-08

Configuring Two Cognito User Pools for a Single AppSync GraphQL API: A YAML CloudFormation Template Guide

Introduction

Amazon Cognito is a user authentication and authorization service provided by AWS. It allows developers to add user sign-up, sign-in, and access control to their web and mobile applications quickly and easily. AWS AppSync is a managed service that makes it easy to develop GraphQL APIs by handling the heavy lifting of securely connecting to data sources like AWS DynamoDB, AWS Lambda, and more.

In this article, we'll explore how to configure two Cognito user pools for a single AppSync GraphQL API using a YAML CloudFormation template. We'll also discuss how to combine the usage of different groups from the two Cognito user pools in the same GraphQL schema.

One of the most interesting aspects of using two Cognito user pools is that you can provide a general authentication system for end users while having a more secure and dynamic mechanism for admin users. With this configuration, you can define different groups of users with different access levels to your API and use them in the same GraphQL schema. By following the steps outlined in this article, you'll be able to create a flexible and secure authentication and authorization system for your AppSync GraphQL API.

There is also a personal addition to this article that explains how we should abstract ourselves from our assumptions and fiercely pursue a learn-by-doing approach to be more effective in problem-solving.

Prerequisites

Before we begin, make sure you have the following prerequisites:

  • AWS Account
  • AWS CLI installed and configured
  • Basic understanding of AWS AppSync and Amazon Cognito

Steps to Configure Two Cognito User Pools for a Single AppSync GraphQL API

1. Create Two Cognito User Pools

The first step is to create two Cognito user pools. Each user pool will represent a different set of users with different access levels. The CloudFormation template for this would be:

Resources:
  EndUsersPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: end-users-pool
      UsernameAttributes:
        - email
      AutoVerifiedAttributes:
        - email
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: true
      Schema:
        - AttributeDataType: String
          Name: email
          Required: true
        - AttributeDataType: String
          Name: name
          Required: false
  EndUsersPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: end-users-pool-client
      UserPoolId: !Ref EndUsersPool
  AdminUsersPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: admin-users-pool
      UsernameAttributes:
        - email
      AutoVerifiedAttributes:
        - email
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: true
      Schema:
        - AttributeDataType: String
          Name: email
          Required: true
        - AttributeDataType: String
          Name: name
          Required: false
  AdminUsersPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: admin-users-pool-client
      UserPoolId: !Ref AdminUsersPool

This template creates a Cognito User Pool named "end-users-pool" with email as the username attribute and email as an auto-verified attribute. It also specifies a password policy and defines two user attributes: email and name. It does the same for the admin user pool named “admin-users-pool”.

In addition, the template creates a User Pool Client named "end-users-pool-client" and associates it with the User Pool created in the previous step. Again, it does the same for the admin users pool named "admin-users-pool" and "admin-users-pool-client".

2. Create AppSync GraphQL API

Next, we'll add an AppSync GraphQL API to our YAML CloudFormation template. Here's an example YAML template for creating an AppSync GraphQL API:

Resources:
  AppSyncApi:
    Type: 'AWS::AppSync::GraphQLApi'
    Properties:
      Name: graphql-api
      AuthenticationType: AMAZON_COGNITO_USER_POOLS
      UserPoolConfig:
        AwsRegion: us-east-1
        DefaultAction: ALLOW
        UserPoolIds:
          - !Ref EndUsersPool
          - !Ref AdminUsersPool

In the YAML template, we define an AppSyncApi resource of type AWS::AppSync::GraphQLApi. We set the Name property to a descriptive name for the API. The AuthenticationType property is set to AMAZON_COGNITO_USER_POOLS to indicate that we'll use Cognito user pools for authentication.

We set the UserPoolConfig property to configure the Cognito user pools for the API. The AwsRegion property specifies the AWS region where the user pools are located. The DefaultAction property sets the default action for users who are not authenticated. The UserPoolIds property is an array of Cognito user pool IDs. In our case, we set it to the IDs of the two user pools we created earlier.

3. Define GraphQL Schema

Next, we'll define the GraphQL schema for our API. In the schema, we'll define two types of users - EndUser and AdminUser - that belong to the two different Cognito user pools.

Here's an example GraphQL schema:

type Query {
  endUserQuery: EndUser
  adminUserQuery: AdminUser
}

type EndUser @aws_auth(cognito_user_pools: [{userPoolId: "EndUsersPool", groups: ["endUsers"]}]) {
  id: ID!
  name: String!
}

type AdminUser @aws_auth(cognito_user_pools: [{userPoolId: "AdminUsersPool", groups: ["adminUsers"]}]) {
  id: ID!
  name:

Possible misleading different syntaxis

You may find a different syntaxis for the same CloudFormation template. The first time I wanted to implement this, I got the following example which uses `@aws_cognito_user_pools` directive with the cognito_groups argument.

type Query {
    endUserQuery: EndUser
   @aws_cognito_user_pools(cognito_groups: ["endUsers"]) 
}

Initially, I thought it was misleading because it didn’t use the user pool id as an argument which made me think: “how does it know which Cognito group belongs to which Cognito user pool?” I just decided to try it, and it worked, but it also suggested that if you have two Cognito groups with the same name in both user pools, the results could be unexpected.

Conclusion

Configuring an AppSync GraphQL API with two Cognito User Pools can seem like a daunting task at first, but with the proper steps and considerations, it can be a straightforward and efficient process. By following the steps outlined in this article, including creating and configuring the user pools, setting up the authorization and authentication in AppSync, and testing the API, we can create a secure and reliable API that meets the needs of our application.

Remember to keep security and scalability in mind throughout the process and consult the AWS documentation for additional guidance or troubleshooting.

Also, remember that It is important to know how our internal thought processes can sometimes prevent us from effectively utilizing certain tools or directives when building applications. In the case of the AWS Amplify CLI and the aws_cognito_user_pools directive, the lack of direct reference to the user pool in the directive's groups argument can lead to confusion and hesitation when implementing this feature. This can cause us to second-guess ourselves and miss out on the benefits that the groups argument can provide.

In situations like this, it may be better to adopt a "just try and see what happens" approach rather than being overly cautious. By taking this approach and experimenting with the directive's different arguments, we may discover new ways to use the tool that we may not have considered before. This approach can lead to a more efficient and effective development process and help us better understand the tools we are working with.

In summary, it is essential not to let our internal thought processes limit our ability to utilize powerful tools and features when building applications. With an open-minded and experimental approach, developers can maximize the available resources and create better applications.