Skip to content

New serverless pattern - cognito-lambda-dynamodb #2704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions cognito-lambda-dynamodb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Amazon Cognito to AWS Lambda to Amazon DynamoDB

This pattern demonstrates how to create a user in [Amazon Cognito](https://aws.amazon.com/cognito/), handle a **Post Confirmation** trigger using an [AWS Lambda](https://aws.amazon.com/lambda/) function, and store user details in [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). Specifically, when a user signs up and confirms their account in Cognito, the Lambda function automatically writes that user's information to a DynamoDB table.

Learn more about this pattern at Serverless Land Patterns: **<< Add the live URL here >>**

> **Important**: This application uses various AWS services and there are costs associated with these services after the Free Tier usage. Please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

---

## Requirements

1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user/role that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
2. [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured.
3. [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
4. [Node.js](https://nodejs.org/en/download/) (10.x or higher).
5. [AWS CDK Toolkit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed (e.g., `npm install -g aws-cdk`).
6. [Docker](https://docs.docker.com/get-docker/) is recommended if you need to bundle Lambda dependencies in certain ways (though for this TypeScript example, it may not be strictly necessary).

---

## Deployment Instructions

1. **Clone the GitHub Repository**
Create a new directory, navigate to that directory in a terminal, and clone the **serverless-patterns** GitHub repository:
```bash
git clone https://github.com/aws-samples/serverless-patterns.git
```
2. Change directory to the pattern directory:
```
cd serverless-patterns/cognito-lambda-dynamodb/cdk
```
3. Install Dependencies:

```
npm install
```

4. Synthesize the AWS CloudFormation Templates:

```
cdk synth
```

5. Deploy the Stack
```
cdk deploy
```
6. Note the Outputs

After deployment, CDK provides outputs such as the UserPoolId and UserPoolClientId. Make sure to save these for reference. They may be required for testing or client-side integration

## How it works

### Cognito User Pool

- A new Amazon Cognito User Pool is created. Users can sign up using their email address. An optional User Pool Client is also created to handle authentication flows.

### Post Confirmation Trigger

- When a user signs up and confirms their email, Cognito invokes the Post Confirmation Lambda function (AddUserPostConfirmationFunc).

### AWS Lambda Handler

- The Lambda function reads attributes from the event (such as sub [the unique user ID], email, and optional name attributes). It then inserts a new item into the DynamoDB table.

### DynamoDB Table

- A DynamoDB table named Users is created with a primary key called UserID. The Lambda function stores user data (UserID, Email, firstName, lastName, etc.) in this table with each new sign-up.

### Result

- Whenever a new user confirms their email in Cognito, an entry is automatically created in the DynamoDB table with that user's information.

## Testing

## Option 1: Manual Sign-Up through Cognito

1. In the Amazon Cognito Console:

- Navigate to **User Pools** and select the **USER-POOL** that was created.
- Choose the **Users** section and manually create a new user or do a user sign-up using the **Hosted UI** or any relevant client (e.g., AWS Amplify).
- After confirming the user, check the **Users** table in Amazon DynamoDB Console to see if the new record appears.

## Option 2: Automated Testing with Jest (E2E Tests)

This project includes an end-to-end test in `cdk/__tests__/e2e/confirm-user-sign-up.test.ts`. By default, the test references environment variables in `cdk/__tests__/constants.ts`. Steps:

1. Populate `REGION`, `USER_POOL_ID`, `CLIENT_USER_POOL_ID`, and `TABLE_NAME` in `cdk/__tests__/constants.ts` (or set them as environment variables before running tests if you prefer).
2. Run:

```bash
npm run test
```

This will perform a sign-up flow using AWS SDK for Cognito, confirm the new user, and then query DynamoDB to validate that the user entry exists.

## Cleanup

1. Delete the stack
```bash
cdk destroy
```
1. Confirm the stack has been deleted
```bash
aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus"
```

---

Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
8 changes: 8 additions & 0 deletions cognito-lambda-dynamodb/cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions cognito-lambda-dynamodb/cdk/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
14 changes: 14 additions & 0 deletions cognito-lambda-dynamodb/cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `npx cdk deploy` deploy this stack to your default AWS account/region
* `npx cdk diff` compare deployed stack with current state
* `npx cdk synth` emits the synthesized CloudFormation template
4 changes: 4 additions & 0 deletions cognito-lambda-dynamodb/cdk/__tests__/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const REGION = "";
export const USER_POOL_ID = "";
export const CLIENT_USER_POOL_ID = "";
export const TABLE_NAME = "Users";
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as given from "../steps/given";
import * as then from "../steps/then";
import * as when from "../steps/when";

describe("When a user Signs up ", () => {
it("Users profile should be saved in DynamoDB", async () => {
const { password, given_name, family_name, email } = given.a_random_user();
console.log("name: ", given_name, family_name);
const userSub = await when.a_user_signs_up(
password,
email,
given_name,
family_name
);

console.log("user: ", userSub);
const ddbUser = await then.user_exists_in_UsersTable(userSub);

console.log("ddbUser: ", ddbUser);

expect(ddbUser.UserID).toMatch(userSub);
});
});
113 changes: 113 additions & 0 deletions cognito-lambda-dynamodb/cdk/__tests__/steps/given.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as cognito from "@aws-sdk/client-cognito-identity-provider";

import Chance from "chance";
import { REGION, USER_POOL_ID, CLIENT_USER_POOL_ID } from "../constants";
const cognitoClient = new cognito.CognitoIdentityProviderClient({
region: REGION,
});

const chance = new Chance();

const userpool = USER_POOL_ID;
const userpoolClient = CLIENT_USER_POOL_ID;

export const a_random_user = () => {
const given_name = chance.first({ nationality: "en" });
const family_name = chance.first({ nationality: "en" });
const password = ensurePasswordPolicy(
chance.string({
length: 12,
pool: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+",
})
);
console.log("password: ", password);
const email = `${given_name}-${family_name}@dev.com`;
return { given_name, family_name, password, email };
};

function ensurePasswordPolicy(password: string): string {
let newPassword = password;
if (!/[a-z]/.test(newPassword))
newPassword += chance.letter({ casing: "lower" });
if (!/[A-Z]/.test(newPassword))
newPassword += chance.letter({ casing: "upper" });
if (!/[0-9]/.test(newPassword))
newPassword += chance.integer({ min: 0, max: 9 }).toString();
if (!/[!@#$%^&*()_+]/.test(newPassword))
newPassword += chance.pickone([
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"_",
"+",
]);
return newPassword; // Ensure the password is still 12 characters long
}

export const an_authenticated_user = async (): Promise<any> => {
const { given_name, family_name, email, password } = a_random_user();

const userPoolId = userpool;
const clientId = userpoolClient;
console.log("userPoolId", userPoolId);
console.log("clientId", clientId);

console.log(`[${email}] - signing up...`);

const command = new cognito.SignUpCommand({
ClientId: clientId,
Username: email,
Password: password,
UserAttributes: [
{ Name: "firstName", Value: given_name },
{
Name: "lastName",
Value: family_name,
},
],
});

const signUpResponse = await cognitoClient.send(command);
const userSub = signUpResponse.UserSub;

console.log(`${userSub} - confirming sign up`);

const adminCommand: cognito.AdminConfirmSignUpCommandInput = {
UserPoolId: userPoolId as string,
Username: userSub as string,
};

await cognitoClient.send(new cognito.AdminConfirmSignUpCommand(adminCommand));

console.log(`[${email}] - confirmed sign up`);

const authRequest: cognito.InitiateAuthCommandInput = {
ClientId: process.env.CLIENT_USER_POOL_ID as string,
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: email,
PASSWORD: password,
},
};

const authResponse = await cognitoClient.send(
new cognito.InitiateAuthCommand(authRequest)
);

console.log(`${email} - signed in`);

return {
username: userSub as string,
name: `${given_name} ${family_name}`,
email,
idToken: authResponse.AuthenticationResult?.IdToken as string,
accessToken: authResponse.AuthenticationResult?.AccessToken as string,
};
};
33 changes: 33 additions & 0 deletions cognito-lambda-dynamodb/cdk/__tests__/steps/then.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
DynamoDBClient,
GetItemCommand,
ScanCommand,
} from "@aws-sdk/client-dynamodb";
import { unmarshall, marshall } from "@aws-sdk/util-dynamodb";
import { REGION, TABLE_NAME } from "../constants";
const ddbClient = new DynamoDBClient({ region: REGION });
export const user_exists_in_UsersTable = async (
userSub: string
): Promise<any> => {
let Item: unknown;
console.log(`looking for user [${userSub}] in table [${TABLE_NAME}]`);

// search for UserID in dynamoDb] Get Item Command
const getItemCommand = new GetItemCommand({
TableName: TABLE_NAME,
Key: {
UserID: { S: userSub },
},
});

const getItemResponse = await ddbClient.send(getItemCommand);
console.log("Get Item Command Response ....", getItemResponse);

if (getItemResponse.Item) {
Item = unmarshall(getItemResponse.Item); // Get the first matching item
}

console.log("found item:", Item);
expect(Item).toBeTruthy();
return Item;
};
51 changes: 51 additions & 0 deletions cognito-lambda-dynamodb/cdk/__tests__/steps/when.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as cognito from "@aws-sdk/client-cognito-identity-provider";
import { REGION, USER_POOL_ID, CLIENT_USER_POOL_ID } from "../constants";

const cognitoClient = new cognito.CognitoIdentityProviderClient({
region: REGION,
});

const userpool = USER_POOL_ID;
const userpoolClient = CLIENT_USER_POOL_ID;

export const a_user_signs_up = async (
password: string,
email: string,
given_name: string,
family_name: string
): Promise<string> => {
const userPoolId = userpool;
const clientId = userpoolClient;
const username = email;

console.log(`[${email}] - signing up...`);

const command = new cognito.SignUpCommand({
ClientId: clientId,
Username: username,
Password: password,
UserAttributes: [
{ Name: "email", Value: email },
{ Name: "custom:firstName", Value: given_name },
{ Name: "custom:lastName", Value: family_name },
],
});

const signUpResponse = await cognitoClient.send(command);
const userSub = signUpResponse.UserSub;

const adminCommand: cognito.AdminConfirmSignUpCommandInput = {
UserPoolId: userPoolId as string,
Username: userSub as string,
};

const result = await cognitoClient.send(
new cognito.AdminConfirmSignUpCommand(adminCommand)
);

console.log("CONFIRM SIGNUP RESPONSE", result);

console.log(`[${email}] - confirmed sign up`);

return userSub as string;
};
6 changes: 6 additions & 0 deletions cognito-lambda-dynamodb/cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { CdkStack } from "../lib/cdk-stack";

const app = new cdk.App();
new CdkStack(app, "UserManagementStack");
Loading