diff --git a/cognito-lambda-dynamodb/README.md b/cognito-lambda-dynamodb/README.md new file mode 100644 index 000000000..698fa4c69 --- /dev/null +++ b/cognito-lambda-dynamodb/README.md @@ -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 diff --git a/cognito-lambda-dynamodb/cdk/.gitignore b/cognito-lambda-dynamodb/cdk/.gitignore new file mode 100644 index 000000000..f60797b6a --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/cognito-lambda-dynamodb/cdk/.npmignore b/cognito-lambda-dynamodb/cdk/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/cognito-lambda-dynamodb/cdk/README.md b/cognito-lambda-dynamodb/cdk/README.md new file mode 100644 index 000000000..9315fe5b9 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/README.md @@ -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 diff --git a/cognito-lambda-dynamodb/cdk/__tests__/constants.ts b/cognito-lambda-dynamodb/cdk/__tests__/constants.ts new file mode 100644 index 000000000..31133ae1f --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/__tests__/constants.ts @@ -0,0 +1,4 @@ +export const REGION = ""; +export const USER_POOL_ID = ""; +export const CLIENT_USER_POOL_ID = ""; +export const TABLE_NAME = "Users"; diff --git a/cognito-lambda-dynamodb/cdk/__tests__/e2e/confirm-user-sign-up.test.ts b/cognito-lambda-dynamodb/cdk/__tests__/e2e/confirm-user-sign-up.test.ts new file mode 100644 index 000000000..5586fb0d0 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/__tests__/e2e/confirm-user-sign-up.test.ts @@ -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); + }); +}); diff --git a/cognito-lambda-dynamodb/cdk/__tests__/steps/given.ts b/cognito-lambda-dynamodb/cdk/__tests__/steps/given.ts new file mode 100644 index 000000000..6db2f21a2 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/__tests__/steps/given.ts @@ -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 => { + 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, + }; +}; diff --git a/cognito-lambda-dynamodb/cdk/__tests__/steps/then.ts b/cognito-lambda-dynamodb/cdk/__tests__/steps/then.ts new file mode 100644 index 000000000..9c0fc57b5 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/__tests__/steps/then.ts @@ -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 => { + 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; +}; diff --git a/cognito-lambda-dynamodb/cdk/__tests__/steps/when.ts b/cognito-lambda-dynamodb/cdk/__tests__/steps/when.ts new file mode 100644 index 000000000..97c7e0a8e --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/__tests__/steps/when.ts @@ -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 => { + 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; +}; diff --git a/cognito-lambda-dynamodb/cdk/bin/cdk.ts b/cognito-lambda-dynamodb/cdk/bin/cdk.ts new file mode 100644 index 000000000..6882df6d8 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/bin/cdk.ts @@ -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"); diff --git a/cognito-lambda-dynamodb/cdk/cdk.json b/cognito-lambda-dynamodb/cdk/cdk.json new file mode 100644 index 000000000..48b0cb77a --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/cdk.json @@ -0,0 +1,88 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true + } +} diff --git a/cognito-lambda-dynamodb/cdk/jest.config.js b/cognito-lambda-dynamodb/cdk/jest.config.js new file mode 100644 index 000000000..911fd44e1 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + testEnvironment: "node", + roots: ["/__tests__"], + testMatch: ["**/*.test.ts"], + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + testTimeout: 90000, +}; diff --git a/cognito-lambda-dynamodb/cdk/lib/cdk-stack.ts b/cognito-lambda-dynamodb/cdk/lib/cdk-stack.ts new file mode 100644 index 000000000..682afa8a2 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/lib/cdk-stack.ts @@ -0,0 +1,111 @@ +import * as cdk from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb"; +import { Runtime } from "aws-cdk-lib/aws-lambda"; +import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as path from "path"; +import { aws_cognito as Cognito } from "aws-cdk-lib"; + +export class CdkStack extends cdk.Stack { + public readonly usersTable: Table; + public readonly addUserToTableFunc: NodejsFunction; + public readonly userPool: Cognito.UserPool; + public readonly userPoolClient: Cognito.UserPoolClient; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // 1. Create the DynamoDB Table + this.usersTable = new Table(this, "UsersTable", { + partitionKey: { + name: "UserID", + type: AttributeType.STRING, + }, + tableName: "Users", // optional: specify the table name + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // This function will be used as the Cognito PostConfirmation trigger + this.addUserToTableFunc = new NodejsFunction( + this, + "AddUserPostConfirmationFunc", + { + functionName: "addUserFunc", // optional + runtime: Runtime.NODEJS_20_X, + handler: "handler", + entry: path.join( + __dirname, + "./functions/AddUserPostConfirmation/handler.ts" + ), + environment: { + REGION: cdk.Stack.of(this).region, + TABLE_NAME: this.usersTable.tableName, + }, + } + ); + + // 3. Grant the Lambda permission to write to the "Users" table + this.addUserToTableFunc.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["dynamodb:PutItem"], + resources: [this.usersTable.tableArn], + }) + ); + + // 4. Create the Cognito User Pool and configure it + this.userPool = new Cognito.UserPool(this, "UserPool", { + userPoolName: "USER-POOL", + selfSignUpEnabled: true, + deletionProtection: false, + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoVerify: { + email: true, + }, + passwordPolicy: { + minLength: 8, + requireLowercase: false, + requireUppercase: false, + requireDigits: false, + requireSymbols: false, + }, + signInAliases: { + email: true, + }, + standardAttributes: { + email: { + required: true, + mutable: true, + }, + }, + customAttributes: { + firstName: new Cognito.StringAttribute({ minLen: 1, maxLen: 50 }), + lastName: new Cognito.StringAttribute({ minLen: 1, maxLen: 50 }), + }, + + // 5. Attach our post-confirmation trigger to the Lambda + lambdaTriggers: { + postConfirmation: this.addUserToTableFunc, + }, + }); + + // 6. Create a User Pool Client + this.userPoolClient = new Cognito.UserPoolClient(this, "UserPoolClient", { + userPool: this.userPool, + authFlows: { + userPassword: true, + userSrp: true, + }, + }); + + // 7. (Optional) Output values for later reference + new cdk.CfnOutput(this, "UserPoolId", { + value: this.userPool.userPoolId, + }); + + new cdk.CfnOutput(this, "UserPoolClientId", { + value: this.userPoolClient.userPoolClientId, + }); + } +} diff --git a/cognito-lambda-dynamodb/cdk/lib/functions/AddUserPostConfirmation/handler.ts b/cognito-lambda-dynamodb/cdk/lib/functions/AddUserPostConfirmation/handler.ts new file mode 100644 index 000000000..2fad6e204 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/lib/functions/AddUserPostConfirmation/handler.ts @@ -0,0 +1,49 @@ +import { PostConfirmationConfirmSignUpTriggerEvent } from "aws-lambda"; +import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; +import { marshall } from "@aws-sdk/util-dynamodb"; + +// Read environment variables +const dynamoRegion = process.env.REGION; +const tableName = process.env.TABLE_NAME; + +// Throw an error if mandatory env variables aren't set +if (!dynamoRegion || !tableName) { + throw new Error( + "Missing mandatory environment variables: 'AWS_REGION' or 'TABLE_NAME'." + ); +} + +const client = new DynamoDBClient({ region: dynamoRegion }); + +export const handler = async ( + event: PostConfirmationConfirmSignUpTriggerEvent +) => { + const date = new Date(); + const isoDate = date.toISOString(); + + const params = { + TableName: tableName, + Item: marshall({ + UserID: event.request.userAttributes.sub, + Email: event.request.userAttributes.email, + firstName: + event.request.userAttributes["custom:firstName"] || + event.request.userAttributes.given_name, + lastName: + event.request.userAttributes["custom:lastName"] || + event.request.userAttributes.family_name, + CreatedAt: isoDate, + __typename: "User", + }), + }; + + try { + await client.send(new PutItemCommand(params)); + console.log("User added to DynamoDB successfully"); + } catch (error) { + console.error("Error adding user to DynamoDB:", error); + throw error; + } + + return event; +}; diff --git a/cognito-lambda-dynamodb/cdk/package.json b/cognito-lambda-dynamodb/cdk/package.json new file mode 100644 index 000000000..fd76ea4ef --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/package.json @@ -0,0 +1,34 @@ +{ + "name": "cdk", + "version": "0.1.0", + "bin": { + "cdk": "bin/cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest jest --verbose ./__tests__/e2e/confirm-user-sign-up.test.ts", + "cdk": "cdk" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.148", + "@types/chance": "^1.1.6", + "@types/jest": "^29.5.14", + "@types/node": "^22.7.9", + "aws-cdk": "2.1005.0", + "esbuild": "^0.25.2", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "@aws-sdk/client-cognito-identity-provider": "^3.782.0", + "@aws-sdk/client-dynamodb": "^3.782.0", + "@aws-sdk/util-dynamodb": "^3.782.0", + "aws-cdk-lib": "2.181.1", + "chance": "^1.1.12", + "constructs": "^10.0.0", + "dotenv": "^16.4.7" + } +} diff --git a/cognito-lambda-dynamodb/cdk/test/cdk.test.ts b/cognito-lambda-dynamodb/cdk/test/cdk.test.ts new file mode 100644 index 000000000..1e6b29c85 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/test/cdk.test.ts @@ -0,0 +1,17 @@ +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as Cdk from '../lib/cdk-stack'; + +// example test. To run these tests, uncomment this file along with the +// example resource in lib/cdk-stack.ts +test('SQS Queue Created', () => { +// const app = new cdk.App(); +// // WHEN +// const stack = new Cdk.CdkStack(app, 'MyTestStack'); +// // THEN +// const template = Template.fromStack(stack); + +// template.hasResourceProperties('AWS::SQS::Queue', { +// VisibilityTimeout: 300 +// }); +}); diff --git a/cognito-lambda-dynamodb/cdk/tsconfig.json b/cognito-lambda-dynamodb/cdk/tsconfig.json new file mode 100644 index 000000000..3b3f3b309 --- /dev/null +++ b/cognito-lambda-dynamodb/cdk/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020", "dom"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"], + "esModuleInterop": true + }, + "exclude": ["node_modules", "cdk.out"] +} diff --git a/cognito-lambda-dynamodb/cognito-lambda-dynamodb.json b/cognito-lambda-dynamodb/cognito-lambda-dynamodb.json new file mode 100644 index 000000000..67ac93d4f --- /dev/null +++ b/cognito-lambda-dynamodb/cognito-lambda-dynamodb.json @@ -0,0 +1,94 @@ +{ + "title": "Amazon Cognito to AWS Lambda to Amazon DynamoDB", + "description": "Create a user in Amazon Cognito, handle a Post Confirmation trigger with AWS Lambda, and store user details in Amazon DynamoDB.", + "language": "TypeScript", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This sample project demonstrates how to create a user in an Amazon Cognito User Pool, then automatically insert that user's details into a DynamoDB table once the user confirms their email. The Post Confirmation Lambda trigger handles the event from Cognito and uses the AWS SDK for JavaScript (v3) to write user data to the DynamoDB table.", + "Key attributes such as the user's unique ID (sub), email, and optional custom attributes are passed to Lambda, which then processes and persists this data. The table is configured in on-demand capacity mode (Pay Per Request) for cost efficiency and minimal management overhead.", + "This pattern deploys a Cognito User Pool, a User Pool Client, a DynamoDB table, and a Node.js AWS Lambda function as the trigger." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns", + "templateURL": "serverless-patterns/cognito-lambda-dynamodb", + "projectFolder": "cognito-lambda-dynamodb/cdk", + "templateFile": "lib/cdk-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Cognito - User sign-up/sign-in with triggers", + "link": "https://aws.amazon.com/cognito/" + }, + { + "text": "AWS Lambda - Serverless compute for triggered actions", + "link": "https://aws.amazon.com/lambda/" + }, + { + "text": "Amazon DynamoDB - Fast and flexible NoSQL database", + "link": "https://aws.amazon.com/dynamodb/" + } + ] + }, + "deploy": { + "text": [ + "cdk synth", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for end-to-end tests using Jest and manual sign-up steps." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Vidit Shah", + "bio": "Software Engineer working @ServerlessCreed,making Serverless Courses and workshops", + "linkedin": "www.linkedin.com/in/vidit-shah", + "twitter": "https://x.com/Vidit_210/", + "image": "https://media.licdn.com/dms/image/v2/D4D03AQHbL_7ZCYfUGQ/profile-displayphoto-shrink_200_200/B4DZUXcQlTGkAY-/0/1739855039564?e=2147483647&v=beta&t=MhOEFqsUDaKnLypK8eYYRqqD8Uq9xHUnijO5tN-fMpc" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "cognito", + "label": "Amazon Cognito" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "dynamodb", + "label": "Amazon DynamoDB" + }, + "line1": { + "from": "icon1", + "to": "icon2", + "label": "" + }, + "line2": { + "from": "icon2", + "to": "icon3", + "label": "" + } + } +} diff --git a/cognito-lambda-dynamodb/example-pattern.json b/cognito-lambda-dynamodb/example-pattern.json new file mode 100644 index 000000000..a068c1796 --- /dev/null +++ b/cognito-lambda-dynamodb/example-pattern.json @@ -0,0 +1,59 @@ +{ + "title": "Amazon Cognito to AWS Lambda to Amazon DynamoDB", + "description": "Create a user in Amazon Cognito, handle a Post Confirmation trigger with AWS Lambda, and store user details in Amazon DynamoDB.", + "language": "TypeScript", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This sample project demonstrates how to create a user in an Amazon Cognito User Pool, then automatically insert that user's details into a DynamoDB table once the user confirms their email. The Post Confirmation Lambda trigger handles the event from Cognito and uses the AWS SDK for JavaScript (v3) to write user data to the DynamoDB table.", + "Key attributes such as the user's unique ID (sub), email, and optional custom attributes are passed to Lambda, which then processes and persists this data. The table is configured in on-demand capacity mode (Pay Per Request) for cost efficiency and minimal management overhead.", + "This pattern deploys a Cognito User Pool, a User Pool Client, a DynamoDB table, and a Node.js AWS Lambda function as the trigger." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns", + "templateURL": "serverless-patterns/cognito-lambda-dynamodb", + "projectFolder": "cognito-lambda-dynamodb/cdk", + "templateFile": "lib/cdk-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Cognito - User sign-up/sign-in with triggers", + "link": "https://aws.amazon.com/cognito/" + }, + { + "text": "AWS Lambda - Serverless compute for triggered actions", + "link": "https://aws.amazon.com/lambda/" + }, + { + "text": "Amazon DynamoDB - Fast and flexible NoSQL database", + "link": "https://aws.amazon.com/dynamodb/" + } + ] + }, + "deploy": { + "text": ["cdk synth", "cdk deploy"] + }, + "testing": { + "text": [ + "See the GitHub repo for end-to-end tests using Jest and manual sign-up steps." + ] + }, + "cleanup": { + "text": ["Delete the stack: cdk destroy."] + }, + "authors": [ + { + "name": "Vidit Shah", + "bio":"Software Engineer working @ServerlessCreed,making Serverless Courses and workshops" + "linkedin": "www.linkedin.com/in/vidit-shah", + "twitter": "https://x.com/Vidit_210/", + "image": "https://media.licdn.com/dms/image/v2/D4D03AQHbL_7ZCYfUGQ/profile-displayphoto-shrink_200_200/B4DZUXcQlTGkAY-/0/1739855039564?e=2147483647&v=beta&t=MhOEFqsUDaKnLypK8eYYRqqD8Uq9xHUnijO5tN-fMpc" + } + ] +}