Using AWS Secrets Manager to rotate a Amazon Cognito user password

Using AWS Secrets Manager to rotate a Amazon Cognito user password

·

10 min read

Recently, I have been working on creating automated tests for an API. For testing, a "tester" API user in a Amazon Cognito user pool is used to perform the authorised test API calls. Following good security practices, the tester API user credentials (username and password) needs to be securely stored and it's password rotated periodically.

Naturally, AWS Secrets Manager was found to be a good solution for this. AWS Secrets Manager can securely store secrets. Our automated API testing setup used a Postman collection run in an AWS Lambda function (to see how, refer to my other blog post here). This could be easily tweaked to retrieve the API user credentials from Secrets Manager. Furthermore, Secrets Manager provides a feature that can automate the periodic rotation of the API user credentials.

In this blog post I will show how to set up Secrets Manager to perform automatic periodic rotation of a users password in a Amazon Cognito user pool.

Pre-requisites and things to know beforehand

This solution uses AWS Secrets Manager and a AWS Lambda function to perform the password rotation. The Lambda function will interact with Amazon Cognito user pool to change the users password.

This solution will require you to have the following:

The solution described here uses the following set up:

  • Amazon Cognito user pool itself is used as the user directory for the user.

  • The auth flow is user password-based authentication. i.e. Cognito user pool client is configured with ALLOW_USER_PASSWORD_AUTH.

  • The Cognito user pool client is configured without a client secret.

Check if the above set up is in agreement with your security requirements. If not, you can use this guide as a starting point, then modify the Secrets Manager rotation Lambda function to meet your own needs.

Create a Cognito user pool and app client

The method used for changing a Cognito user password requires a Cognito user pool with a client that allows user password-authentication (ALLOW_USER_PASSWORD_AUTH). In this type of authentication flow, Cognito receives the password in the authentication request. This is less secure than using secure remote password protocol (SRP) (ALLOW_USER_SRP_AUTH) but it is easier to implement in password rotation Lambda function code. Furthermore, the user pool client is configured without a client secret to also reduce code complexity.

It is possible to write code that supports SRP authentication flow and client secret to rotate the password (perhaps I will cover this in a future post).

If you do not have an existing Cognito user pool, you can create one with default settings using the following command. Replace freddy-user-pool with your own desired user pool name.

aws cognito-idp create-user-pool \
--pool-name freddy-user-pool

Take note of the Cognito user pool ID in the output of the command.

{
    "UserPool": {
        "Id": "ap-southeast-2_8Ab53SN8i",
        "Name": "freddy-user-pool",
    ...
}

Note that at the time of writing, a new Cognito user pool default password policy contains the following requirements:

  • Minimum length 8 characters

  • Contains at least 1 number

  • Contains at least 1 special character

  • Contains at least 1 uppercase letter

  • Contains at least 1 lowercase letter

Next, using the following command create a user pool client with ALLOW_USER_PASSWORD_AUTH flow. Replace ap-southeast-2_8Ab53SN8 with your user pool ID and freddy-client with your own desired client name.

aws cognito-idp create-user-pool-client \
--user-pool-id ap-southeast-2_8Ab53SN8i \
--client-name freddy-client \
--explicit-auth-flows \
 ALLOW_CUSTOM_AUTH \
 ALLOW_REFRESH_TOKEN_AUTH \
 ALLOW_USER_PASSWORD_AUTH

From the output, take note of the client ID.

{
  "UserPoolClient": {
      "UserPoolId": "ap-southeast-2_8Ab53SN8i",
      "ClientName": "freddy-client",
      "ClientId": "13eah9bc6lrga1hnkeekc1b329",
      ...
  }
}

Create a Lambda function for Cognito user password rotation

AWS Secrets Manager will call a Lambda function to carry out the rotation of the Cognito user password. You can find the Lambda function code here: https://github.com/Freddy-CLH-Blog/secrets-manager-rotation-function-cognito-user/blob/main/src/lambda_function.py

Lambda function IAM role

Create a IAM role for the Lambda function with the trust policy below. This allows the AWS Lambda service to assume the IAM role.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

Add to the Lambda function IAM role the following permissions policy below. Note that the Resource field restricts Secrets Manager access permissions to secrets with the name pattern:

CognitoUserPool/freddy-user-pool/AutoRotatedPasswordUsers/*

Change the AWS_REGION and AWS_ACCOUNT_ID to your own AWS region and account ID.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "SecretsManagerEntry",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:DescribeSecret",
                "secretsmanager:GetSecretValue",
                "secretsmanager:PutSecretValue",
                "secretsmanager:UpdateSecretVersionStage"
            ],
            "Resource": [
                "arn:aws:secretsmanager:AWS_REGION:AWS_ACCOUNT_ID:secret:CognitoUserPool/freddy-user-pool/AutoRotatedPasswordUsers/*"
            ]
        },
        {
            "Sid": "SecretsManagerGenPassword",
            "Effect": "Allow",
            "Action": "secretsmanager:GetRandomPassword",
            "Resource": "*"
        },
        {
            "Sid": "CloudWatchLogs",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

Lambda function for secret rotation

Create a new Lambda function with runtime Python3.12 and use the IAM role that you have created above.

Below is a screenshot of the creation of the Lambda function named "SecretsManagerLambdaRotation-CognitoUser" using the AWS Console.

In the default lambda_function.py file, copy and paste the code from https://github.com/Freddy-CLH-Blog/secrets-manager-rotation-function-cognito-user/blob/main/src/lambda_function.py. Save and deploy the Lambda function.

This Lambda function code uses AWS Lambda Powertools for logging capabilities. To include this library, you can configure the use of the AWS Lambda Powertools public Lambda layer. To do this in the AWS Lambda console, in your Lambda function under Code tab, in the Layers section, choose Add a layer. With AWS layers checked, select AWSLambdaPowertoolsPythonV2 and select the latest version available to you, the choose Add.

The Lambda function needs to be configured with a resource-based policy which allows Secrets Manager to invoke it. To do this in the AWS Lambda console, in your Lambda function under Configuration tab, in the Resource-based policy statements, choose Add permissions. Configure the AWS principal secretsmanager.amazonaws.com with permission to perform lambda:InvokeFunction. See the screenshot below for the configured fields.

How the Lambda function rotates a Cognito user password

Secrets Manager will invoke the Lambda function four times to carry out each of the rotation steps. The steps are:

  1. createSecret

  2. setSecret

  3. testSecret

  4. finishSecret

For more information about these steps, see https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_lambda-functions.html

Each invocation event payload contains the following parameters

  • Step - the rotation step with the values as above

  • SecretId - the ID or ARN of the secret stored in Secrets Manager.

  • ClientRequestToken - Unique identifier for a secret rotation request that is common across the four steps. This is used as the VersionId for a newly generated secret.

Below is an example invocation event for the setSecret step.

{
  "Step": "setSecret",
  "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-name-abc123",
  "ClientRequestToken": "MyClientRequestToken-ABC123-XYZ987",
}

The setSecret step is where the main work is done to change the Cognito user password by making calls to Cognito user pool API. The calls made are as follows:

First, a InitiateAuth call with AuthFlow set to USER_PASSWORD_AUTH is made. We then proceed to change the password for the following two scenarios:

  • In the scenario where the InitiateAuth results in a completed authentication, it's response will contain a AccessToken. We can then make a ChangePassword call with the AccessToken and new password resulting in changing the Cognito user's password.

  • In the scenario where InitiateAuth results with a challenge to force the change of the password (such as when a new user is created with a temporary password). It's response will contain ChallengeName = NEW_PASSWORD_REQUIRED and a Session token. We then make a RespondToAuthChallenge call with the Session token and new password resulting in changing the Cognito user's password.

Create a user in the Cognito user pool

The user which requires periodic password rotation needs to be created in a Cognito user pool. To do this from the Amazon Cogntio console, in your user pool, under Users tab, choose Create user.

For the minimum fields, enter a User name and Email address. The email address does not need to be real and we will not be verifying it. To prevent a password being sent to the email address, under Temporary password, check Set a password then enter a temporary password. Keep a note of the user name temporary password for later entry in to Secrets Manager.

Create a auto rotating secret for the Cognito user password in Secrets Manager

We are now finally able to set up Secrets Manager to securely store the Cognito user password and perform automatic periodic password rotation.

To create a secret entry from AWS Secret Manager Console, choose Store a new secret.

In Secret type section, check Other type of secret. In the Key/value pairs section enter the following key values then choose Next.

Key NameValueExample
clientidYour Congito user pool client ID created in this step.13eah9bc6lrga1hnkeekc1b329
usernameYour Cognito user username created in this step.freddy-tester
passwordYour Cognito user temporary password created in this step.Temp-Password-123

In the Secret name and description, enter a Secret name that matches the name pattern in the Resources field of your Lambda function IAM role permissions policy for Secrets Manager access (created in this step). Provide an optional Description then choose Next.

In the Configure automatic rotation section, enable Automatic rotation. In the Rotation schedule section, set up your desired rotation schedule.

Check the tick box for "Rotate immediately when the secret is stored".

Choose your own desired Rotation schedule.

In the Rotation function section, under Lambda rotation function, select your Secrets Manager Lambda rotation function then choose Next.

In the final step - Review, choose Store.

Check and test the rotated Cognito user password

If you followed along with the previous step, the Cognito user password would have have been rotated immediately after storing it in Secrets Manager. You can choose to rotate the password at any time by selecting the secret entry in Secrets Manager, then under the Rotation tab, choose Rotate secret immediately.

Still within the Rotation tab, scrolling further down you can see the Last rotated date and Next rotation date. Your Lambda rotation function is also shown, which is hyperlinked to take you to your Lambda function page in the AWS Console.

In your Lambda function page, under the Monitor tab, you will be able to see CloudWatch Metrics such as the invocations. You can also navigate to the logs stored in CloudWatch logs by choosing View CloudWatch logs.

The Lambda function produces logs for each step of the secret rotation (each being it's own Lambda invocation event). AWS Lambda Powertools logger is used to produce log messages in JSON format and add the following logging keys:

  • secret_id

  • rotation_step

  • version_id

For a "nicer" viewing of the secret rotation logs, you can use the following CloudWatch Log insights query on you Lambda function logs.

display @timestamp, level, secret_id, rotation_step, version_id, message
| filter rotation_step in ["createSecret", "setSecret", "testSecret", "finishSecret"]
| sort @timestamp asc

Below is an example of the log messages produced for a successful Secrets Manager rotation of the Cognito user password, obtained using the above CloudWatch Log insights query.

To test that the rotated Cognito user password works, you can retrieve it from Secrets Manager and then perform a Cognito user pool InitiateAuth call.

To retrieve the Cognito user credentials from Secrets Manager, navigate to your secret, then in the Overview tab select Retrieve secret value.

From the retrieved secret value, you can then use the clientId, username and password to perform the Cognito user pool InitiateAuth call using the AWS CLI.

For example:

Use a JSON file to handle parsing special characters in the password which will be needed as an argument to the AWS CLI command.

initiate-auth.json

{
    "AuthFlow": "USER_PASSWORD_AUTH",
    "AuthParameters": {
        "USERNAME": "freddy-tester",
        "PASSWORD": "AuXHv`E;XUA24)lX#Lc9z[LiC-Og{^SU",
    },
    "ClientId": "13eah9bc6lrga1hnkeekc1b329"
}

Then run the following AWS CLI command.

aws cognito-idp initiate-auth --cli-input-json file://initiate-auth.json

A successful call will return the authentication tokens.

{
    "AuthenticationResult": {
        "AccessToken": ".....",
        "ExpiresIn": 3600,
        "TokenType": "Bearer",
        "RefreshToken": ".....",
        "IdToken": "....."
    }
}

Conclusion

In this blog post we used Secrets Manager to securely store and rotate the password of a user in a Cognito user pool. A Lambda function is created using Python to perform the rotation logic. For simplicity, the Cognito user pool is configured with a client that uses password-based authentication ALLOW_USER_PASSWORD_AUTH and no client secret. You may want to consider more secure authentication configuration which will also require changes to the secret rotation Lambda function.