Table of contents
- Pre-requisites and things to know beforehand
- Create a Cognito user pool and app client
- Create a Lambda function for Cognito user password rotation
- Create a user in the Cognito user pool
- Create a auto rotating secret for the Cognito user password in Secrets Manager
- Check and test the rotated Cognito user password
- Conclusion
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:
Basic familiarity with Amazon Cognito user pools (see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html).
Basic knowledge on creating a AWS Lambda function (see https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html).
AWS CLI installed - Used in some steps to quickly create AWS resources (see https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-install.html)
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:
createSecret
setSecret
testSecret
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 aboveSecretId
- 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 theVersionId
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 theAccessToken
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 aSession
token. We then make a RespondToAuthChallenge call with theSession
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 Name | Value | Example |
clientid | Your Congito user pool client ID created in this step. | 13eah9bc6lrga1hnkeekc1b329 |
username | Your Cognito user username created in this step. | freddy-tester |
password | Your 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.