Cognito has a PostConfirmation trigger which sends a confirmed user’s data to a lambda.
I kept running into cyclical dependencies when declaring the lambda I want to trigger while also creating a user pool in the same Stack.
In the end I decided to send the confirmed user data to a queue, then have a decoupled lambda consume those messages and update the user.
We need to create the Queue, then define/pull in our Lambda function. Pass in our queue url as an environment variable so our Lambda knows where to post messages.
The Lambda function needs to have permission to send messages to our queue, then we also need to tell our User Pool to trigger our Lambda on Post Confirmation.
// CognitoStack.js
const yourQueueName = new Queue(this, "YourQueueName", {
visibilityTimeout: Duration.seconds(30),
});
const postConfirmationFunction = new NodejsFunction(
this,
"PostConfirmationFunction",
{
...NODE_CONFIG_DEFAULTS,
entry: "src/adapters/users/postConfirmationFunction.js",
bundling: getDefaultBundling(),
environment: {
USER_UPDATE_QUEUE_URL: yourQueueName.queueUrl,
},
}
);
yourQueueName.grantSendMessages(postConfirmationFunction);
yourUserPool.addTrigger(
UserPoolOperation.POST_CONFIRMATION,
postConfirmationFunction
);
The Lambda receives an object that contains userPoolId and userName. Username is actually the sub id on the user in the pool. The same can be found on request.userAttributes.sub
. This ID is how I find the user to update.
// postConfirmationFunction.js
{
version: '1',
region: 'us-east-1',
userPoolId: '',
userName: '14c8e468...',
callerContext: {
awsSdkVersion: 'aws-sdk-unknown-unknown',
clientId: '1e77...'
},
triggerSource: 'PostConfirmation_ConfirmSignUp',
request: {
userAttributes: {
sub: '14c8e468...',
email_verified: 'true',
'cognito:user_status': 'CONFIRMED',
email: '[email protected]'
}
},
response: {}
}
The Lambda strips off the userName and userPoolId and sends it off to the queue for processing.
// postConfirmationFunction.js
export const handler = async (event) => {
const { userName, userPoolId } = event;
try {
const sqsParams = {
QueueUrl: yourQueueNameUrl,
MessageBody: JSON.stringify({ userName, userPoolId }),
};
await sqs.send(new SendMessageCommand(sqsParams));
// ....
};
We need to create another Lambda that has permission to consume messages from the new queue.
This Lambda also needs permission to make updates to the users in the pool. We can export and share these values between Stacks as props.
For this Lambda to be triggered we have to specify an event source. For this we use the SqsEventSource and pass in the specific queue.
// RestApiStack.js
const messageConsumerFunction = new NodejsFunction(
this,
"MessageConsumerFunction",
{
...NODE_CONFIG_DEFAULTS,
entry: "src/adapters/users/messageConsumerFunction.js",
bundling: getDefaultBundling(),
}
);
props.cognitoUserPool.grant(
messageConsumerFunction,
"cognito-idp:AdminUpdateUserAttributes",
"cognito-idp:AdminGetUser",
"cognito-idp:ListUsers"
);
props.userUpdateQueue.grantConsumeMessages(messageConsumerFunction); // read sqs messages
messageConsumerFunction.addEventSource(
new SqsEventSource(props.userUpdateQueue, {
batchSize: 1,
maxBatchingWindow: Duration.seconds(0),
})
);
SQS messages come in as an array of Record objects, so we parse the body which is a JSON string. This is the message we actually sent.
{
Records: [
{
messageId: "5edac985...",
receiptHandle: "AQEBhZZ...",
body: '{"userName":"14c8e468...","userPoolId":"us-east-1..."}', // JSON STRING
attributes: [Object],
messageAttributes: {},
md5OfBody: "5aae1f5b...",
eventSource: "aws:sqs",
eventSourceARN: "arn:aws:sqs:us-east-1:112...",
awsRegion: "us-east-1",
},
];
}
We can then use the userName and userPoolId to find our user and update their attributes.
// messageConsumerFunction.js
export const handler = async (event) => {
const messageBody = JSON.parse(event.Records[0].body);
const { userName, userPoolId } = messageBody;
const userParams = {
UserPoolId: userPoolId,
Filter: `sub = "${userName}"`,
Limit: 1,
};
try {
const userData = await cognitoIdentityServiceProvider
.listUsers(userParams)
.promise();
if (!userData.Users || userData.Users.length === 0) {
throw new Error(`User with ID ${userName} not found`);
}
const user = userData.Users[0];
const tenantIdAttribute = user.Attributes.find(
(attr) => attr.Name === "custom:tenantId"
);
if (tenantIdAttribute) {
console.log(
`User ${userName} already has tenantId: ${tenantIdAttribute.Value}`
);
return event;
}
const tenantId = generateUUID();
const params = {
UserPoolId: userPoolId,
Username: userName,
UserAttributes: [
{
Name: "custom:tenantId",
Value: tenantId,
},
{
Name: "custom:isAdmin",
Value: "true",
},
],
};
await cognitoIdentityServiceProvider
.adminUpdateUserAttributes(params)
.promise();
return event;
} catch (error) {
console.error("Error fetching/updating user attributes:", error);
throw new Error("Failed to fetch or update tenant ID");
}
};