Skip to content

Instantly share code, notes, and snippets.

@singledigit
Last active January 16, 2024 16:15
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"
@tommelo
Copy link

tommelo commented Aug 2, 2017

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

@gryzinsky
Copy link

Hey @tommelo, i don't think it's possible in the moment, at least not through the official AWS::Cognito::UserPool resource type. AWS cfn documentation actually describes two properties available for User Pool resources called AliasAttributes and AutoVerifiedAttributes, which are related to Username sign-in strategy and administrative management. We're trying to find a custom template which implements username attributes, which is available through aws-cli using create-user-pool with --username-attributes and CreateUserPool Api through UsernameAttributes.

In the meanwhile we're using aws-cli to build our User Pools. I think this may be enough in the general case. After deployment it's unlikely for you to change your user pool sign-in / sign-up strategy, so I think this approach may solve our problem right now! :)

@subvind
Copy link

subvind commented Aug 23, 2017

@singledigit How could one get this to work with email verification instead of text message?

Copy link

ghost commented Aug 25, 2017

You could try creating the pool you want manually, then print out a JSON spec of it to describe the pool using Amazon's https://github.com/awslabs/aws-shell or perhaps aws-cli, not sure about aws-cli.

screen shot 2017-08-25 at 2 25 10 pm

You can then convert the JSON to YAML to see the differences more clearly.

Or to import the spec in order to create a new pool, you need to remove the properties like DateCreated, PoolID, etc which are specific to the previous pool. The aws-shell command is:

screen shot 2017-08-25 at 2 36 20 pm

Here is a link to docs

Hope this helps.

@bluepeter
Copy link

I am having the same problem. Thanks @heraldsmit for your suggestion. Unfortunately, it appears Cognito and CloudFormation just don't mix... or at least, it's not possible to create a Cognito with email as the username. In the AWS console, I created two Cognito User Pools that were exactly the same apart from the manual selection of email as the login option. Then, I ran cognito-idp and diffed the output. Nothing of consequence:

$ aws --profile tyger cognito-idp describe-user-pool --user-pool-id us-west-2_U43IvMWv4 > a.json
$ aws --profile tyger cognito-idp describe-user-pool --user-pool-id us-west-2_bA5VHNFpz > b.json
$ diff a.json b.json 
122c122
<                 "Required": true, 
---
>                 "Required": false, 
219c219
<         "Name": "sdcvsdf", 
---
>         "Name": "zdf", 
222c222
<         "LastModifiedDate": 1512023600.734, 
---
>         "LastModifiedDate": 1512023676.889, 
246c246
<         "CreationDate": 1512023600.041, 
---
>         "CreationDate": 1512023675.659, 
248c248
<         "Id": "us-west-2_U43IvMWv4", 
---
>         "Id": "us-west-2_bA5VHNFpz", 

The Required: true simply refers to the fact that, by default, when the "username" option is selected, the email address as a standard attribute is checked. When you select to user email address as the login, it gets unchecked. This has no consequence on the login choice.

@bluepeter
Copy link

Note per this AWS thread it is "coming soon" to CloudFormation (but that was back in July, so who knows): https://forums.aws.amazon.com/thread.jspa?threadID=259349&tstart=0

@bsdkurt
Copy link

bsdkurt commented Feb 28, 2018

Hey @tommelo. You can do this using a custom resource that uses a lamba. Here is a working example:

https://github.com/bsdkurt/aws-node-custom-user-pool

@ldgarcia
Copy link

@trabur, like this:

  UserPool:
    Type: 'AWS::Cognito::UserPool'
    Properties:
      AutoVerifiedAttributes:
      - email

@ldgarcia
Copy link

@tommelo I think this should work:

  UserPool:
    Type: 'AWS::Cognito::UserPool'
    Properties:
      AliasAttributes:
      - email
      - phone_number

@illuminatedspace
Copy link

This gist is a miracle. Thanks so much.

@mtinra
Copy link

mtinra commented Jun 13, 2018

Finally, they update the "use an email address or phone number as their “username”"
now you can use:

UsernameAttributes: [email]

@rhysmccaig
Copy link

Great gist, thanks!

@MustSeeMelons
Copy link

Great gist, one question though, how could I add FB/Google login options on top of this?
I have added them to the IdentityPool, created seperate auth/unauth roles for each, but cant seem to figure out how to properly configure the role attachment.

@nodox
Copy link

nodox commented Aug 22, 2018

+1. How do you get this working with Fb/Google?

@thrixton
Copy link

@MustSeeMelons, @nodox, see the SO answer List of CloudFormation CognitoEvents for Cognito Identity Pool creation and the elements under "SupportedLoginProviders", worked for me.

@frankleonrose
Copy link

frankleonrose commented Nov 5, 2018

I can successfully create a UserPool, UserPoolGroup, and UserPoolUser in my CloudFormation stack. However, I have been unable to put the user in the group with a UserPoolUserToGroupAttachment resource. @singledigit, have you had any luck with that?

  DashboardAdminGroupAssignment:
    DependsOn:
      - DashboardUserPool (UserPool created in stack)
      - DashboardAdminGroup (UserPoolGroup created in stack with name 'Administrators')
      - DashboardAdministrator (UserPoolUser created in stack with email address DashboardAdminEmail)
    Type: AWS::Cognito::UserPoolUserToGroupAttachment
    Properties:
      UserPoolId: !Ref DashboardUserPool
      GroupName: Administrators
      Username: "6cec517c-8888-4973-9999-60e725379121" (Works! However, not useful when the user was created in the same stack and the sub would be unknown aforehand.)
      # Username: !Ref DashboardAdministrator (yields error: Unknown User)
      # Username: !Sub ${DashboardAdminEmail} (yields error: Unknown User)
      # Username: !GetAtt DashboardAdministrator.sub (yields error: Unknown User)

@jamesgraham
Copy link

@frankleonrose did you manage to work that one out?

@ctunna
Copy link

ctunna commented Jun 28, 2019

@jamesgraham, I'm also interested in a response from @frankleonrose

@frankleonrose
Copy link

Sorry, @jamesgraham & @ctunna, I stopped working on that back then and never did get it to work. Of course, things may be different now >6 months later!

@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

@RandomInternetDude
Copy link

RandomInternetDude 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