Cognito User Pool Tips & Tricks
Tags: cognito, serverless, aws
Published: 2021-11-16 20:18:40

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).

## Deletion Policy
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.

## Case Insensitive Username
Make the user name case **insensitive** to make sure someone creating an account with *Myname@domain.com* can also log in with *myname@domain.com*. 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
```

## Password Policy
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
```

## Token Validity

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
```

More details about these options [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpoolclient-tokenvalidityunits.html) and [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpoolclient.html#cfn-cognito-userpoolclient-accesstokenvalidity).

## Enable all standard attributes
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](https://docs.aws.amazon.com/cognito/latest/developerguide/limits.html#resource-quotas) 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.

## Admin Only Registration
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
```

## Required attributes
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.

## Mutable 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 attribute
The "address" field is a bit confusing. According to the documentation they follow the [OpenID Connect specification](http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) 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.

## Storing JSON in attributes
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.

## Configure Read & Write attributes
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](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SchemaAttributeType.html) to prevent users from writing to the attribute however AWS recommends to use [ReadAttributes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpoolclient.html#cfn-cognito-userpoolclient-readattributes) & [WriteAttributes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpoolclient.html#cfn-cognito-userpoolclient-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
```

## Modifying the ID Token
Cognito allows for quite some customization using [Lambda Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html). 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.

## Conclusion
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.