How to setup cross-account Cognito User Pool migration with the Migrate User Lambda Trigger
I would like to setup cross-account user migration between two of my Cognito user pools. How can I configure the migrate user Lambda trigger to seamlessly implement this migration? Are there any code templates available for the migrate user Lambda trigger?
Short description
Cognito User Pools are able to integrate with the Lambda service using a feature known as Lambda Triggers. These are Lambda Functions that can be automatically executed at specific stages of User Pool authentication in order to enhance the behavior of the authentication flow. This article addresses one specific Lambda Trigger; the Migrate User Lambda Trigger. This is a Lambda Function which executes during a user's sign-in attempt (or a password reset attempt) if the user does not already exist in the user pool. If the Lambda Function completes successfully, then it creates the user in the new user pool.
This functionality of the Migrate User Lambda Trigger has several applications, but a very common use of this Lambda Trigger is in implementing a "just-in-time" (JIT) user migration flow from one User Pool to another. While it is possible to manually import users into a new User Pool with a CSV file, this Migrate User Lambda Trigger method is useful when you would like to seamlessly migrate existing user accounts from an old User Pool to a new User Pool during their normal sign-in process. This article provides step-by-step instructions (with template Python and NodeJS scripts) to setup this cross-account user migration flow between two User Pools.
Resolution
This user migration implementation will perform the following steps:
- A user tries to authenticate or reset password with the new User Pool
- The new User Pool triggers the Migrate User Lambda Trigger function if the user is not found in the new User Pool
- The Migrate User Lambda Trigger function assumes a Role in the old User Pool's Account
- The Migrate User Lambda Trigger function uses the assumed Role to authenticate the user in the old User Pool, and get the user's attributes
- If the user is successfully authenticated with the old User Pool, then the validated user is created in the new User Pool
To implement this user migration setup, you must first have the following AWS resources deployed:
- A user pool and app client deployed in an old/source AWS account, for example 111111111111
- A user pool and app client deployed in a new/destination AWS account, for example 222222222222
NOTE: In these steps, we will refer to the old/source User Pool's Account as '111111111111' and the new/destination User Pool's Account as '222222222222'. These Account numbers will be different in your actual AWS environment.
STEP 1: Setup configuration in the old/source User Pool's Account (Account 111111111111)
- Enable the
ALLOW_ADMIN_USER_PASSWORD_AUTH
authentication flow for the old/source User Pool:- AWS Cognito Console -> User Pools -> (select old User Pool) -> App integration tab -> App client list -> (select old App Client) -> App client information -> select Edit -> Add
ALLOW_ADMIN_USER_PASSWORD_AUTH
in Authentication flows -> Finish by selecting Save changes
- AWS Cognito Console -> User Pools -> (select old User Pool) -> App integration tab -> App client list -> (select old App Client) -> App client information -> select Edit -> Add
- Create an IAM Role for the Migrate User Lambda Function to assume and authenticate users in the old User Pool:
- AWS IAM Console -> Roles -> Create role
- Trusted entity type -> Account -> 222222222222
- Role name:
CognitoCrossAccountMigrationRole
- Finish by selecting Create role
- AWS IAM Console -> Roles -> Create role
- Assign the appropriate permissions to the IAM Role:
- AWS IAM Console -> Roles -> (select
CognitoCrossAccountMigrationRole
) -> Add permissions -> Create inline policy- Policy name:
CognitoCrossAccountMigrationPolicy
- Create the following policy in the JSON Policy Editor, and finish by selecting Create policy
- Policy name:
- AWS IAM Console -> Roles -> (select
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PermissionForUserPoolActions",
"Effect": "Allow",
"Action": [
"cognito-idp:AdminInitiateAuth",
"cognito-idp:AdminGetUser"
],
"Resource": "*"
}
]
}
STEP 2: Setup configuration in the new/destination User Pool's Account (Account 222222222222)
- Enable the
ALLOW_ADMIN_USER_PASSWORD_AUTH
authentication flow for the new/destination User Pool:- AWS Cognito Console -> User Pools -> (select new User Pool) -> App integration tab -> App client list -> (select new App Client) -> App client information -> select Edit -> Add
ALLOW_ADMIN_USER_PASSWORD_AUTH
in Authentication flows -> Finish by selecting Save changes
- AWS Cognito Console -> User Pools -> (select new User Pool) -> App integration tab -> App client list -> (select new App Client) -> App client information -> select Edit -> Add
- Create the Migrate User Lambda Function new/destination User Pool's Account:
- AWS Lambda Console -> Functions -> Create function -> Author from scratch
- Function name:
CognitoCrossAccountMigrationFunction
- Runtime: Node.js 20.x or Python 3.12
- Finish by selecting Create function
- Function name:
- AWS Lambda Console -> Functions -> Create function -> Author from scratch
- Add the following code to the Migrate User Lambda Function. Select the appropriate code for your specific setup:
- AWS Lambda Console -> Functions -> (select
CognitoCrossAccountMigrationFunction
) -> Code tab -> Select and copy the appropriate code from the following list into the Lambda function's index.mjs or lambda_function.py file. NOTE: Code templates are provided at the bottom of this article in the 'Code' section- MigrateUserLambdaFunction-NodeJS (Node.js 20.x function runtime, App Client without Client Secret)
- MigrateUserLambdaFunction-NodeJS-ClientSecret (Node.js 20.x function runtime, App Client with Client Secret)
- MigrateUserLambdaFunction-Python (Python 3.12 function runtime, App Client without Client Secret)
- MigrateUserLambdaFunction-Python-ClientSecret (Python 3.12 function runtime, App Client with Client Secret)
- Update the following variables in the code with the values from the old/source User Pool:
- sourceAccountRoleARN (
CognitoCrossAccountMigrationRole
in this example) - sourceAccountUserPoolId
- sourceAccountClientId
- sourceAccountRegion (Only necessary if User Pools are cross-region)
- sourceAccountClientSecret (Only necessary if the old App Client has a Client Secret configured)
- sourceAccountRoleARN (
- Finish by selecting Deploy
- AWS Lambda Console -> Functions -> (select
- Assign the appropriate permissions to the Lambda Function's Execution Role:
- AWS Lambda Console -> Functions -> (select
CognitoCrossAccountMigrationFunction
) -> Configuration tab -> Permissions -> (select Execution Role) -> Add permissions -> Create inline policy- Policy name:
CognitoCrossAccountMigrationAssumeRolePolicy
- Create the following policy in the JSON Policy Editor, and finish by selecting Create policy
- Policy name:
- AWS Lambda Console -> Functions -> (select
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PermissionToAssumeRole",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::111111111111:role/CognitoCrossAccountMigrationRole"
}
]
}
- Set the Lambda Function as the Migrate User Lambda Trigger in the new/destination User Pool:
- AWS Cognito Console -> User Pools -> (select new User Pool) -> User pool properties tab -> Lambda triggers -> Add Lambda trigger -> (Sign-up, Migrate user trigger) -> Lambda function -> (select
CognitoCrossAccountMigrationFunction
) - Finish by selecting Add Lambda trigger
- AWS Cognito Console -> User Pools -> (select new User Pool) -> User pool properties tab -> Lambda triggers -> Add Lambda trigger -> (Sign-up, Migrate user trigger) -> Lambda function -> (select
You should now have a completed user migration setup! Now it's time to test it.
STEP 3: Test the cross-account user migration
- (Optional) Setup and execute a test event in Lambda to test the Migrate User Lambda Trigger function:
- AWS Lambda Console -> Functions -> (select
CognitoCrossAccountMigrationFunction
) -> Code tab -> Test -> Configure test event -> Create new event- Create the following test event in the JSON Event Editor, and finish by selecting Save (Be sure to replace the appropriate values with the parameters from your old User Pool)
- AWS Lambda Console -> Functions -> (select
{
"version": "1",
"triggerSource": "UserMigration_Authentication",
"region": "ENTER-SOURCE-REGION",
"userPoolId": "ENTER-SOURCE-APP-CLIENT-ID",
"userName": "ENTER-SOURCE-TEST-USERNAME",
"callerContext": {
"awsSdkVersion": "aws-sdk-unknown-unknown",
"clientId": "ENTER-SOURCE-APP-CLIENT-ID"
},
"request": {
"password": "ENTER-SOURCE-TEST-USERS-PASSWORD",
"validationData": "None",
"userAttributes": "None"
},
"response": {
"userAttributes": "None",
"forceAliasCreation": "None",
"enableSMSMFA": "None",
"finalUserStatus": "None",
"messageAction": "None",
"desiredDeliveryMediums": "None"
}
}
- Attempt to sign-in to the new User Pool with a user from the old User Pool. Use the Hosted UI of the new User Pool to sign-in:
- AWS Cognito Console -> User Pools -> (select new User Pool) -> App integration tab -> App client list -> (select new App Client) -> Hosted UI -> select View Hosted UI
- Sign-in with credentials from the old/source User Pool
- If the user successfully authenticates, then the user is also created in the new User Pool:
- AWS Cognito Console -> User Pools -> (select new User Pool) -> Users tab
Related information
- Migrate user Lambda trigger
- Approaches for migrating users to Amazon Cognito user pools
- How do I change the attributes of an Amazon Cognito user pool after creation?
- Importing users into user pools from a CSV file
Code
- MigrateUserLambdaFunction-NodeJS
// MigrateUserLambdaFunction-NodeJS
// Migrate User Lambda Trigger Template - Cross-Account User Pool Migration
// NodeJS 20.x runtime
// AWS SDK for Javascript v3
// Declare source Account variables. Enter the information for your User Pool from your source Account
const sourceAccountRoleARN = "ENTER-SOURCE-ROLE-ARN";
const sourceAccountRegion = "ENTER-SOURCE-REGION";
const sourceAccountUserPoolId = "ENTER-SOURCE-USER-POOL-ID";
const sourceAccountClientId = "ENTER-SOURCE-APP-CLIENT-ID";
// Declare global variables and import required AWS SDK libraries
import { CognitoIdentityProvider, AdminGetUserCommand, AdminInitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider";
import { STS, AssumeRoleCommand } from "@aws-sdk/client-sts";
const stsclient = new STS();
//const stsclient = new STS({ region: sourceAccountRegion });
// Import cross-account credentials
var paramsAssumeRole = {
RoleArn: sourceAccountRoleARN,
RoleSessionName: "CrossAccountCognitoMigration"
};
const {Credentials} = await stsclient.send(new AssumeRoleCommand(paramsAssumeRole));
const tempCredentialsObj = {
accessKeyId: Credentials.AccessKeyId,
secretAccessKey: Credentials.SecretAccessKey,
sessionToken: Credentials.SessionToken
};
const cognitoidpclient = new CognitoIdentityProvider({ credentials: tempCredentialsObj });
//const cognitoidpclient = new CognitoIdentityProvider({ credentials: tempCredentialsObj, region: sourceAccountRegion });
// Main Function
export const handler = async (event, context, callback) => {
// Logs event to CloudWatch Logs. Useful for basic script troubleshooting
console.log(event);
// Declare local variables
let user;
// Basic logic handling
if (event.triggerSource == "UserMigration_Authentication") {
// Authenticate the User with the existing source User Pool
user = await authenticateUser(event.userName, event.request.password);
let userAttributes = {};
if (user.Username) {
user.UserAttributes.forEach(attribute => {
if (attribute.Name == "sub") {
return;
}
userAttributes[attribute.Name] = attribute.Value;
});
event.response.userAttributes = userAttributes;
event.response.finalUserStatus = "CONFIRMED";
event.response.messageAction = "SUPPRESS";
context.succeed(event);
} else {
// Return error to Amazon Cognito
callback("Bad password");
}
} else if (event.triggerSource == "UserMigration_ForgotPassword") {
// Lookup the user in the existing source User Pool
user = await getUserPoolUser(event.userName);
if (user.Username) {
let userAttributes = {};
user.UserAttributes.forEach(attribute => {
if (attribute.Name == "sub") {
return;
}
userAttributes[attribute.Name] = attribute.Value;
});
// NOTE: The 'email_verified' or 'phone_number_verified' attribute must be
// set to 'true' in order to enable password-reset code to be sent to the
// user. If the attribute is not already set in the source user pool, you
// can uncomment the following line of code to set the 'email_verified' or
// 'phone_number_verified' attribue as verified:
// userAttributes['email_verified'] = "true";
// userAttributes['phone_number_verified'] = "true";
event.response.userAttributes = userAttributes;
event.response.messageAction = "SUPPRESS";
context.succeed(event);
} else {
// Return error to Amazon Cognito
callback("Bad password");
}
} else {
// Return error to Amazon Cognito
callback("Bad triggerSource " + event.triggerSource);
}
};
// Function 1 - authenticateUser - Authenticates the User with the source User Pool
async function authenticateUser(username, password) {
let rez = "";
const paramsInitiateAuth = {
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
ClientId: sourceAccountClientId,
UserPoolId: sourceAccountUserPoolId,
AuthParameters: {
USERNAME: username,
PASSWORD: password
}
};
const authrez = await cognitoidpclient.send(new AdminInitiateAuthCommand(paramsInitiateAuth));
if (authrez.hasOwnProperty("AuthenticationResult")) {
rez = getUserPoolUser(username);
}
return rez;
}
// Function 2 - getUserPoolUser - Gets User information from source User Pool
async function getUserPoolUser(username) {
let rez = "";
const paramsGetUser = {
UserPoolId: sourceAccountUserPoolId,
Username: username
};
rez = await cognitoidpclient.send(new AdminGetUserCommand(paramsGetUser));
return rez;
}
- MigrateUserLambdaFunction-NodeJS-ClientSecret
// MigrateUserLambdaFunction-NodeJS-ClientSecret
// Migrate User Lambda Trigger Template - Cross-Account User Pool Migration (w/ Client Secret)
// NodeJS 20.x runtime
// AWS SDK for Javascript v3
// Declare source Account variables. Enter the information for your User Pool from your source Account
const sourceAccountRoleARN = "ENTER-SOURCE-ROLE-ARN";
const sourceAccountRegion = "ENTER-SOURCE-REGION";
const sourceAccountUserPoolId = "ENTER-SOURCE-USER-POOL-ID";
const sourceAccountClientId = "ENTER-SOURCE-APP-CLIENT-ID";
const sourceAccountClientSecret = "ENTER-SOURCE-APP-CLIENT-SECRET";
// Declare global variables and import required AWS SDK libraries
import { createHmac } from 'crypto';
import { CognitoIdentityProvider, AdminGetUserCommand, AdminInitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider";
import { STS, AssumeRoleCommand } from "@aws-sdk/client-sts";
const stsclient = new STS();
//const stsclient = new STS({ region: sourceAccountRegion });
// Import cross-account credentials
var paramsAssumeRole = {
RoleArn: sourceAccountRoleARN,
RoleSessionName: "CrossAccountCognitoMigration"
};
const {Credentials} = await stsclient.send(new AssumeRoleCommand(paramsAssumeRole));
const tempCredentialsObj = {
accessKeyId: Credentials.AccessKeyId,
secretAccessKey: Credentials.SecretAccessKey,
sessionToken: Credentials.SessionToken
};
const cognitoidpclient = new CognitoIdentityProvider({ credentials: tempCredentialsObj });
//const cognitoidpclient = new CognitoIdentityProvider({ credentials: tempCredentialsObj, region: sourceAccountRegion });
// Main Function
export const handler = async (event, context, callback) => {
// Logs event to CloudWatch Logs. Useful for basic script troubleshooting
console.log(event);
// Declare local variables
let user;
// Basic logic handling
if (event.triggerSource == "UserMigration_Authentication") {
// Authenticate the User with the existing source User Pool
user = await authenticateUser(event.userName, event.request.password);
let userAttributes = {};
if (user.Username) {
user.UserAttributes.forEach(attribute => {
if (attribute.Name == "sub") {
return;
}
userAttributes[attribute.Name] = attribute.Value;
});
event.response.userAttributes = userAttributes;
event.response.finalUserStatus = "CONFIRMED";
event.response.messageAction = "SUPPRESS";
context.succeed(event);
} else {
// Return error to Amazon Cognito
callback("Bad password");
}
} else if (event.triggerSource == "UserMigration_ForgotPassword") {
// Lookup the user in the existing source User Pool
user = await getUserPoolUser(event.userName);
if (user.Username) {
let userAttributes = {};
user.UserAttributes.forEach(attribute => {
if (attribute.Name == "sub") {
return;
}
userAttributes[attribute.Name] = attribute.Value;
});
// NOTE: The 'email_verified' or 'phone_number_verified' attribute must be
// set to 'true' in order to enable password-reset code to be sent to the
// user. If the attribute is not already set in the source user pool, you
// can uncomment the following line of code to set the 'email_verified' or
// 'phone_number_verified' attribue as verified:
// userAttributes['email_verified'] = "true";
// userAttributes['phone_number_verified'] = "true";
event.response.userAttributes = userAttributes;
event.response.messageAction = "SUPPRESS";
context.succeed(event);
} else {
// Return error to Amazon Cognito
callback("Bad password");
}
} else {
// Return error to Amazon Cognito
callback("Bad triggerSource " + event.triggerSource);
}
};
// Function 1 - authenticateUser - Authenticates the User with the source User Pool
async function authenticateUser(username, password) {
let rez = "";
// Calculate secret hash
const hasher = createHmac("sha256", sourceAccountClientSecret);
hasher.update( username + sourceAccountClientId );
//hasher.update(`${username}${sourceAccountClientId}`);
const secrethash = hasher.digest("base64");
const paramsInitiateAuth = {
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
ClientId: sourceAccountClientId,
UserPoolId: sourceAccountUserPoolId,
AuthParameters: {
USERNAME: username,
PASSWORD: password,
SECRET_HASH: secrethash
}
};
const authrez = await cognitoidpclient.send(new AdminInitiateAuthCommand(paramsInitiateAuth));
if (authrez.hasOwnProperty("AuthenticationResult")) {
rez = getUserPoolUser(username);
}
return rez;
}
// Function 2 - getUserPoolUser - Gets User information from source User Pool
async function getUserPoolUser(username) {
let rez = "";
const paramsGetUser = {
UserPoolId: sourceAccountUserPoolId,
Username: username
};
rez = await cognitoidpclient.send(new AdminGetUserCommand(paramsGetUser));
return rez;
}
- MigrateUserLambdaFunction-Python
# MigrateUserLambdaFunction-Python
# Migrate User Lambda Trigger Template - Cross-Account User Pool Migration
# Python 3.12 runtime
# AWS SDK for Python Boto3
# Import required Python libraries
import json
import boto3
# Declare source Account variables. Enter the information for your User Pool from your source Account
sourceAccountRoleARN = 'ENTER-SOURCE-ROLE-ARN'
#sourceAccountRegion = 'ENTER-SOURCE-REGION'
sourceAccountUserPoolId = 'ENTER-SOURCE-USER-POOL-ID'
sourceAccountClientId = 'ENTER-SOURCE-APP-CLIENT-ID'
# Import cross-account credentials
stsclient = boto3.client('sts')
response = stsclient.assume_role(
RoleArn = sourceAccountRoleARN,
RoleSessionName = 'CrossAccountCognitoMigration'
)
temp_credentials = response['Credentials']
cognitoidpclient = boto3.client(
'cognito-idp',
aws_access_key_id = temp_credentials['AccessKeyId'],
aws_secret_access_key = temp_credentials['SecretAccessKey'],
aws_session_token = temp_credentials['SessionToken'],
)
# Main function
def lambda_handler(event, context):
# Basic logic handling
if (event['triggerSource'] == 'UserMigration_Authentication'):
user = cognitoidpclient.admin_initiate_auth(
UserPoolId = sourceAccountUserPoolId,
ClientId = sourceAccountClientId,
AuthFlow = 'ADMIN_USER_PASSWORD_AUTH',
AuthParameters = {
'USERNAME': event['userName'],
'PASSWORD': event['request']['password']
}
)
if (user):
userAttributes = cognitoidpclient.get_user(
AccessToken = user['AuthenticationResult']['AccessToken']
)
for userAttribute in userAttributes['UserAttributes']:
if userAttribute['Name'] == 'email':
userEmail = userAttribute['Value']
event['response']['userAttributes'] = {
"email": userEmail,
"email_verified": "true"
}
event['response']['messageAction'] = "SUPPRESS"
print (event)
return (event)
else:
return('Bad Password')
elif (event['triggerSource'] == 'UserMigration_ForgotPassword'):
user = cognitoidpclient.admin_get_user(
UserPoolId = sourceAccountUserPoolId,
Username = event['userName']
)
if (user):
for userAttribute in user['UserAttributes']:
if userAttribute['Name'] == 'email':
userEmail = userAttribute['Value']
event['response']['userAttributes'] = {
"email": userEmail,
"email_verified": "true"
}
event['response']['messageAction'] = "SUPPRESS"
print (event)
return (event)
else:
return('Bad Password')
else:
return('There was an error')
- MigrateUserLambdaFunction-Python-ClientSecret
# MigrateUserLambdaFunction-Python-ClientSecret
# Migrate User Lambda Trigger Template - Cross-Account User Pool Migration (w/ Client Secret)
# Python 3.12 runtime
# AWS SDK for Python Boto3
# Import required Python libraries
import json
import boto3
import sys
import hmac, hashlib, base64
# Declare source Account variables. Enter the information for your User Pool from your source Account
sourceAccountRoleARN = 'ENTER-SOURCE-ROLE-ARN'
#sourceAccountRegion = 'ENTER-SOURCE-REGION'
sourceAccountUserPoolId = 'ENTER-SOURCE-USER-POOL-ID'
sourceAccountClientId = 'ENTER-SOURCE-APP-CLIENT-ID'
sourceAccountClientSecret = 'ENTER-SOURCE-APP-CLIENT-SECRET'
# Import cross-account credentials
stsclient = boto3.client('sts')
response = stsclient.assume_role(
RoleArn = sourceAccountRoleARN,
RoleSessionName = 'CrossAccountCognitoMigration'
)
temp_credentials = response['Credentials']
cognitoidpclient = boto3.client(
'cognito-idp',
aws_access_key_id = temp_credentials['AccessKeyId'],
aws_secret_access_key = temp_credentials['SecretAccessKey'],
aws_session_token = temp_credentials['SessionToken'],
)
# Main function
def lambda_handler(event, context):
# Basic logic handling
if (event['triggerSource'] == 'UserMigration_Authentication'):
# Calculate secret hash
message = bytes(event['userName'] + sourceAccountClientId,'utf-8')
key = bytes(sourceAccountClientSecret,'utf-8')
secrethash = base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()
user = cognitoidpclient.admin_initiate_auth(
UserPoolId = sourceAccountUserPoolId,
ClientId = sourceAccountClientId,
AuthFlow = 'ADMIN_USER_PASSWORD_AUTH',
AuthParameters = {
'USERNAME': event['userName'],
'PASSWORD': event['request']['password'],
'SECRET_HASH': secrethash
}
)
if (user):
userAttributes = cognitoidpclient.get_user(
AccessToken = user['AuthenticationResult']['AccessToken']
)
for userAttribute in userAttributes['UserAttributes']:
if userAttribute['Name'] == 'email':
userEmail = userAttribute['Value']
event['response']['userAttributes'] = {
"email": userEmail,
"email_verified": "true"
}
event['response']['messageAction'] = "SUPPRESS"
print (event)
return (event)
else:
return('Bad Password')
elif (event['triggerSource'] == 'UserMigration_ForgotPassword'):
user = cognitoidpclient.admin_get_user(
UserPoolId = sourceAccountUserPoolId,
Username = event['userName']
)
if (user):
for userAttribute in user['UserAttributes']:
if userAttribute['Name'] == 'email':
userEmail = userAttribute['Value']
event['response']['userAttributes'] = {
"email": userEmail,
"email_verified": "true"
}
event['response']['messageAction'] = "SUPPRESS"
print (event)
return (event)
else:
return('Bad Password')
else:
return('There was an error')
Relevant content
- asked a year agolg...
- asked 2 years agolg...
- Accepted Answerasked 6 years agolg...
- AWS OFFICIALUpdated 6 months ago
- AWS OFFICIALUpdated 2 months ago
- AWS OFFICIALUpdated a year ago
- AWS OFFICIALUpdated a year ago