Skip to content

Instantly share code, notes, and snippets.

@singledigit
Last active December 11, 2024 10:03
Show Gist options
  • Save singledigit/2c4d7232fa96d9e98a3de89cf6ebe7a5 to your computer and use it in GitHub Desktop.
Save singledigit/2c4d7232fa96d9e98a3de89cf6ebe7a5 to your computer and use it in GitHub Desktop.
Create a Cognito Authentication Backend via CloudFormation
AWSTemplateFormatVersion: '2010-09-09'
Description: Cognito Stack
Parameters:
AuthName:
Type: String
Description: Unique Auth Name for Cognito Resources
Resources:
# Creates a role that allows Cognito to send SNS messages
SNSRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "cognito-idp.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: "CognitoSNSPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "sns:publish"
Resource: "*"
# Creates a user pool in cognito for your app to auth against
# This example requires MFA and validates the phone number to use as MFA
# Other fields can be added to the schema
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
UserPoolName: !Sub ${AuthName}-user-pool
AutoVerifiedAttributes:
- phone_number
MfaConfiguration: "ON"
SmsConfiguration:
ExternalId: !Sub ${AuthName}-external
SnsCallerArn: !GetAtt SNSRole.Arn
Schema:
- Name: name
AttributeDataType: String
Mutable: true
Required: true
- Name: email
AttributeDataType: String
Mutable: false
Required: true
- Name: phone_number
AttributeDataType: String
Mutable: false
Required: true
- Name: slackId
AttributeDataType: String
Mutable: true
# Creates a User Pool Client to be used by the identity pool
UserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
ClientName: !Sub ${AuthName}-client
GenerateSecret: false
UserPoolId: !Ref UserPool
# Creates a federeated Identity pool
IdentityPool:
Type: "AWS::Cognito::IdentityPool"
Properties:
IdentityPoolName: !Sub ${AuthName}Identity
AllowUnauthenticatedIdentities: true
CognitoIdentityProviders:
- ClientId: !Ref UserPoolClient
ProviderName: !GetAtt UserPool.ProviderName
# Create a role for unauthorized acces to AWS resources. Very limited access. Only allows users in the previously created Identity Pool
CognitoUnAuthorizedRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Federated: "cognito-identity.amazonaws.com"
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
StringEquals:
"cognito-identity.amazonaws.com:aud": !Ref IdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": unauthenticated
Policies:
- PolicyName: "CognitoUnauthorizedPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "mobileanalytics:PutEvents"
- "cognito-sync:*"
Resource: "*"
# Create a role for authorized acces to AWS resources. Control what your user can access. This example only allows Lambda invokation
# Only allows users in the previously created Identity Pool
CognitoAuthorizedRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Federated: "cognito-identity.amazonaws.com"
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
StringEquals:
"cognito-identity.amazonaws.com:aud": !Ref IdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": authenticated
Policies:
- PolicyName: "CognitoAuthorizedPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "mobileanalytics:PutEvents"
- "cognito-sync:*"
- "cognito-identity:*"
Resource: "*"
- Effect: "Allow"
Action:
- "lambda:InvokeFunction"
Resource: "*"
# Assigns the roles to the Identity Pool
IdentityPoolRoleMapping:
Type: "AWS::Cognito::IdentityPoolRoleAttachment"
Properties:
IdentityPoolId: !Ref IdentityPool
Roles:
authenticated: !GetAtt CognitoAuthorizedRole.Arn
unauthenticated: !GetAtt CognitoUnAuthorizedRole.Arn
Outputs:
UserPoolId:
Value: !Ref UserPool
Export:
Name: "UserPool::Id"
UserPoolClientId:
Value: !Ref UserPoolClient
Export:
Name: "UserPoolClient::Id"
IdentityPoolId:
Value: !Ref IdentityPool
Export:
Name: "IdentityPool::Id"
@alessandrojcm
Copy link

Hmm, I'm setting the AutoVerifiedAttributes property to email; but on deployment, AWS complains that the user pool is not configured to set SMS. Anyone has any ideas on why is that happening?

@ctunna
Copy link

ctunna commented Jul 17, 2019

@jamesgraham @frankleonrose

Thanks for getting back to me.

I opened a support case with Amazon and they eventually got back to me. Apparently it's a problem with the username attributes set to email only but they've opened a feature request to support this behavior. As a workaround they suggested making the group association with a Lambda in a Custom Resource. Yuck.

Hello,

Thank you very much for your patience.

I have examined the UserPoolId: “us-east-1_abcd” and noticed that the ‘Username Attributes’ is set to ‘email’ only. The ’AWS::Cognito::UserPoolUserToGroupAttachment’ uses the ‘AdminAddUserToGroup’ API [1] and the API expects an 'username' and not an email alias. CloudFormation calls Cognito APIs in order to create the resources, and when you configure users to sign up and sign in with email, the SignUp API generates a persistent UUID for your user, and uses it as the immutable username attribute internally. This UUID has the same value as the Ref claim in the user identity token. For more information about this behavior, please refer "User Signs Up and Signs In with Email or Phone Number Instead of Username" [3]. We cannot use Ref in this situation because the UserPool “myuserpool” is configured as sign-up and sign-in with email, then the ‘AWS::Cognito::UserPoolUser’ [2] will return email address as Ref and not username.

To replicate this issue, I performed the following steps:

Created a UserPool with the same setting that I noticed in your UserPoolId: “us-east-1_abcd” using the following template:
---
Resources:
UserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
UserPoolName: ddv1
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
UsernameAttributes:
- email
---
Once the UserPool was created, I used your CFN template and I could see the same error that you are encountering “User does not exist (Error Code: UserNotFoundException)”.

The property "UsernameAttributes: - email" in the resource "AWS::Cognito::UserPool" is not allowing the attachment of the user to group. The property "UsernameAttributes: - email" specifies whether email addresses or phone numbers can be specified as usernames when a user signs up [4]. After removing this property, I was able to deploy the stack with no issues. Below is the snippet for your reference:
---
Resources:
UserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
UserPoolName: ddv2
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
---

Based on this behavior, I suggest you to use the following workaround in order to fix the issue you are facing:

  • Create a new UserPool with default settings.
    OR
  • Remove the property "UsernameAttributes" from the resource "AWS::Cognito::UserPool", if you want to set an username with an email.

I have attached 3 sample templates that I have tested in my test environment and confirm that they are working and a few document references which I think you may find useful.

Please let me know how it goes and in case you have any questions or need further information and I will be glad to assist you further.

References:

[1] AdminAddUserToGroup - https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminAddUserToGroup.html

[2] AWS::Cognito::UserPoolUser - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpooluser.html

[3] Configuring User Pool Attributes - Option 2: User Signs Up and Signs In with Email or Phone Number Instead of Username - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html?shortFooter=true#user-pool-settings-aliases-settings-option-2

[4] AWS::Cognito::UserPool - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html

To see the file named 'ddv3.yml,ddv4.yml,ddv5.yml' included with this correspondence, please use the case link given below the signature.

Best regards,

Dushyant D.
Amazon Web Services

Hello,

Thank you for your patience. I understand that you have a specific use case where you have an existing user pool with many users and creating a new user pool is not possible in your case.

I have researched further for other available options in working with my colleagues from Cognito support team and I have been made aware of the other option which are:

  1. Migrating the users to new user pool because we cannot modify the attributes once the user pool is created. This article[1] might help you to understand how to change the attributes of an Amazon Cognito user pool after creation. If you have issues migrating the users to new user pool please contact the Cognito team as they are more proficient with the Cognito service.

  2. If migrating is not feasible, I have tested that only putting the UUID (sub attribute)[2] directly in the ‘Username’ property of resource type 'AWS::Cognito::UserPoolUserToGroupAttachment' works fine. Below is the snippet for your reference:

    CustomerUserPoolToGroupAttachment:
    Type: 'AWS::Cognito::UserPoolUserToGroupAttachment'
    DependsOn:
    - CustomerUserPoolUser
    - CustomerUserGroup
    Properties:
    GroupName: !Ref Customer
    UserPoolId: !Ref UserPoolId
    Username: '8abcd885e-1111-2222-3333-1abcd2345e'
    #Username: !Ref CustomerUsername

The resource type 'AWS::Cognito::UserPoolUser' only returns the name of the user[3]; therefore I have raised a feature request on your behalf to our internal CloudFormation team. AWS feature requests do not have an ETA and therefore I am unable to provide timelines of availability. You can follow our Release History page link[4] for important changes in each release of the AWS CloudFormation.

However, we can create a 'sub' attribute through a AWS CloudFormation custom resource backed by a Lambda function which returns the UUID when you !GetAtt. You can achieve this by coding the logic to retrieve the 'sub' property using the 'ListUsers' API call. For more details on how Custom Resources works please refer to document link[5] provided below. I have also included for AWS Lambda-backed Custom Resources link[6] along with an example[7], which shows how to dynamically lookup AMI IDs for an EC2 instances. This example demonstrates the mechanism for returning values to CloudFormation stack in the ResponseObject(A1)[8] and retrieving value from the ResponseObject returned by the Lambda function backing the custom resource, using Fn::GetAtt intrinsic function(A2). Below is just the logic for writing a custom resource backed by a Lambda function, writing code for custom resources is considered out of scope for support but I will assist you to the best of my abilities.

(A1) Example: Custom Resource Lambda Code Snippet:

ec2.describeImages(describeImagesParams, function(err, describeImagesResult) {
    if (err) {
        responseData = {Error: "DescribeImages call failed"};
        console.log(responseData.Error + ":\n", err);
    }
    else {
        var images = describeImagesResult.Images;
        // Sort images by name in descending order. The names contain the AMI version, formatted as YYYY.MM.Ver.
        images.sort(function(x, y) { return y.Name.localeCompare(x.Name); });
        for (var j = 0; j < images.length; j++) {
            if (isBeta(images[j].Name)) continue;
            responseStatus = "SUCCESS";
            responseData["Id"] = images[j].ImageId;  <=== **** responseData returned by Lambda function code
            break;
        }
    }
    sendResponse(event, context, responseStatus, responseData);
});

};

......

function sendResponse(event, context, responseStatus, responseData) {
var responseBody = JSON.stringify({
Status: responseStatus,
Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData <=== **** returned in the ResponseObject as 'Data' object
});

Example: Template Snippet

"Resources" : {
"SampleInstance": {
"Type": "AWS::EC2::Instance",
"Properties": {
"InstanceType" : { "Ref" : "InstanceType" },
"ImageId": { "Fn::GetAtt": [ "AMIInfo", "Id" ] } <=== **** GetAtt function retrieving 'Id' key from 'Data' key of the custom resource's ('AMIInfo') response
}
},
"AMIInfo": {
"Type": "Custom::AMIInfo", <== Custom resource backed by Lambda
"Properties": {
"ServiceToken": { "Fn::GetAtt" : ["AMIInfoFunction", "Arn"] },
"Region": { "Ref": "AWS::Region" },
"Architecture": { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] }
}
},

For your specific use case the custom resource can lookup ‘sub’ attributes by making the ListUsers[2] API call, with the passed in 'UserPoolId:' and 'Username:' properties and return 'sub' property in the 'Data' object of the response as { ‘sub’: }, in ResponseObject which allows (!GetAtt .sub) to retrieve the UUID of the user as shown in the snippet(B)

(B) Sample Snippet for Custom Resource:

CustomUserPoolUserDetails:
Type: "Custom::CustomUserPoolUserDetails"
Properties:
ServiceToken:
!Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunctionName}
UserPoolId: !Ref UserPoolId
Username: !Ref Username

CustomerUserPoolToGroupAttachment:
Type: 'AWS::Cognito::UserPoolUserToGroupAttachment'
DependsOn:
- CustomerUserPoolUser
- CustomerUserGroup
Properties:
GroupName: !Ref Customer
UserPoolId: !Ref UserPoolId
Username: !GetAtt CustomUserPoolUserDetails.sub

*** LambdaFunctionName *** : This would the lambda function with the logic to retrieve the 'sub' property of the UserPoolUser using ListUsers[] API.

In case you have any further queries or concerns, please do let me know and I will be more than happy to assist you further.

References:

[1] How do I change the attributes of an Amazon Cognito user pool after creation? - https://aws.amazon.com/premiumsupport/knowledge-center/cognito-change-user-pool-attributes/

[2] ListUsers API call - https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/list-users.html#examples

[3] AWS::Cognito::UserPoolUser - Return Values - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpooluser.html#aws-resource-cognito-userpooluser-return-values

[4] Release History - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/ReleaseHistory.html

[5] Details on how Custom Resources works - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html

[6] AWS Lambda-backed Custom Resources - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

[7] Walkthrough: Looking Up Amazon Machine Image IDs - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/walkthrough-custom-resources-lambda-lookup-amiids.html

[8] Custom Resource Response Objects - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html

Best regards,

Dushyant D.
Amazon Web Services

@MrRandomInternetDude
Copy link

MrRandomInternetDude commented Jul 25, 2019

Hey @singledigit that helped a lot!
Quick question: Do you know how to allow users to use an email address or phone number as their “username” to sign up and sign in using through Cloudformation template?

cognito

use this below and put it under properties
UsernameAttributes:
- email
AutoVerifiedAttributes:
- emai

@patrickfoley
Copy link

This was super helpful (I was stuck on IdentityPoolRoleMapping and the two IAM roles - this got me over that hump). Thanks so much!

@multiojuice
Copy link

This is great -- thanks :_)

@ecv-raphaeljambalos
Copy link

Big help, thanks man!

@frank-liu
Copy link

Hey @singledigit that helped a lot!
Quick question: Do you know how to allow users to use an email address or phone number as their “username” to sign up and sign in using through Cloudformation template?
cognito

    [
      
    ](https://user-images.githubusercontent.com/5882081/28852186-b055b6d6-76fd-11e7-9916-40e6e0b50377.gif)
    
        ![cognito](https://user-images.githubusercontent.com/5882081/28852186-b055b6d6-76fd-11e7-9916-40e6e0b50377.gif)
      
    
      
        
          
        
        
          
          
        
      
      [
        
          
        
      ](https://user-images.githubusercontent.com/5882081/28852186-b055b6d6-76fd-11e7-9916-40e6e0b50377.gif)

use this below and put it under properties UsernameAttributes: - email AutoVerifiedAttributes: - emai

UsernameAttributes:
- phone_number
- email

@bannarisoftwares
Copy link

bannarisoftwares commented Oct 31, 2022

How to add custom read and write attributes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment