When implementing Cognito in a project I ran into some things that others might run in too. Hopefully this article will prevent that. Nearly all of these tips & tricks assume you are defining your stack using CloudFormation. I would highly recommend using Infrastructure as Code for all of your projects as manually setting up infrastructure by clicking in the console is error prone and doesn't give you the ability to easily remove your resources and/or to roll out multiple environments (or to start over if you messed up something).
Accidents with stacks can happen so first of all, make sure to set the Deletion Policy to Retain to ensure you don't lose precious user data when something unexpected happens to your stack.
Resources: CognitoUserPool: Type: AWS::Cognito::UserPool DeletionPolicy: Retain
Optionally, highly recommended, you can choose to add some logic based on environment so you only set Retain on your production environment and set to Delete (which is the default) on other environments.
Make the user name case insensitive to make sure someone creating an account with Myname@domain.com can also log in with firstname.lastname@example.org. Do not count on your users ability to always type their email address or nickname exactly the same.
Resources: CognitoUserPool: Type: AWS::Cognito::UserPool Properties: UsernameConfiguration: CaseSensitive: False
Set a proper password policy, I went with the configuration below:
Resources: CognitoUserPool: Type: AWS::Cognito::UserPool Properties: Policies: PasswordPolicy: MinimumLength: 12 RequireLowercase: True RequireNumbers: True RequireSymbols: True RequireUppercase: True
You can easily configure the validity of your tokens. I decided to keep Access + ID Tokens valid for 1 day and the refresh token is valid for 30 days.
Resources: CognitoUserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: TokenValidityUnits: AccessToken: days IdToken: days RefreshToken: days AccessTokenValidity: 1 IdTokenValidity: 1 RefreshTokenValidity: 30
When describing a user pool you need to define which attributes you want to use. While the console and documentation clearly state you can not enable standard attributes once the user pool has been created this is easily overlooked when using CloudFormation.
Since enabling these attributes does not cost anything and you might need them in the future I would recommend to configure all of them regardless if you need them now. If you attempt to add the standard attributes later on it will work but they are added as custom attributes instead (and will be prefixed with custom:). There is a limit of 50 on custom attributes. The limit of 50 seems to be changed recently as other documentation pages still mention 25 as the limit which I ran into myself earlier this year too.
If you only plan to create new users server side, you should prevent users from creating their own accounts:
Resources: CognitoUserPool: Type: AWS::Cognito::UserPool Properties: AdminCreateUserConfig: AllowAdminCreateUserOnly: True
Similar to enabling standard attributes, it's not possible to change the required attributes after your user pool has been created. Make sure you verify your use cases and see if you've configured the required attributes correctly.
Resources: CognitoUserPool: Type: AWS::Cognito::UserPool Properties: Schema: - Name: nickname AttributeDataType: String Required: true
If you're only going to use Admin API commands to manage accounts, you can still create accounts without providing these attributes.
If you want to have attributes that can be written only once you can configure them to be immutable:
Resources: CognitoUserPool: Type: AWS::Cognito::UserPool Properties: Schema: - Name: registration AttributeDataType: String Mutable: false
You could for instance use this to write certain details on registration which are written once that never can be changed again.
The "address" field is a bit confusing. According to the documentation they follow the OpenID Connect specification however it's not really clear how to populate this field. The simple solution is you need to submit it as a JSON string and then it kind of accepts anything, not only the documented fields.
Similar to the address attribute you can of course store JSON in other standard or custom attributes too to prevent hitting the limit of custom fields. While you might have enough available custom attributes now, you don't know what you need to add in the future. You could for instance store the last login information (timestamp, country code, IP address) as JSON and use only a single custom field for it and if you later decide to also store the user agent, you can just add it without sacrificing another custom attribute.
When creating a user pool client it is possible to configure which attributes are readable and writable by the user itself. This is something you can easily forget and can be pretty dangerous if you want to manage this only from a server after doing your own validation. If for instance you keep track of a users payment status with a custom attribute the user can just write a simple script and update the status themselves using their own personal access token and the Cognito API. Also, there might be attributes that you just use internally without wanting to expose these to the end user. You could use the DeveloperOnlyAttribute to prevent users from writing to the attribute however AWS recommends to use ReadAttributes & WriteAttributes for it now.
With ReadAttributes you can control which attributes are available to read by the end user. These are also the attributes that will be returned in the ID Token generated by Cognito.
With WriteAttributes you can control which are available to write (modify) by the end user. If you do not define any writeable attribute, by default Cognito will make all your attributes writeable. To prevent this I created the custom attribute "client_storage" which I made writeable just to make sure all the other attributes are now no longer writeable by the end user.
Resources: CognitoUserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: ReadAttributes: - given_name - family_name - nickname - email - picture - email_verified - custom:meta WriteAttributes: - custom:client_storage
Cognito allows for quite some customization using Lambda Triggers. One of these things is to modify the data available in the ID Token.
In the above example you can see the "custom:meta" read attribute which contains a few details on the user that could be useful to the client (ie: account status or payment status). You can transform any existing attribute, remove them or add new ones.
Overall I'm very excited about Cognito User Pools as it is super powerful. On the other hand it's easy to make (crucial) mistakes with the configuration that will haunt you forever. I hope these tips are helpful to others.