In the previous post, I showed you how to create a simple S3 bucket. Next, in this article, I will guide you to create a Cognito User Pool.
Amazon Cognito is an identity platform for web and mobile apps. It’s a user directory, an authentication server, and an authorization service for OAuth 2.0 access tokens and AWS credentials. With Amazon Cognito, you can authenticate and authorize users from the built-in user directory, from your enterprise directory, and from consumer identity providers like Google and Facebook.
Amazon Cognito has two main components:
As step 11 of the previous post. We add a new stack below the Api Stack with the name auth stack like the code below:
new AuthStack(this, `${id}-auth-stack`, { stackName: `${id}-auth-stack`, });
Step 12: Enter the below code to lib/stacks/auth-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { CognitoResource } from '../resources'; export class AuthStack extends Stack { constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); this.createCognito(this, id); } private createCognito(stack: Stack, id: string) { new CognitoResource(stack, `${id}-cognito`, {}) .setupUserPool() .setupAppClient() .setupDomain() .build(); } }
Step 12: Enter the below code to lib/resources/cognito/index.ts
import { Duration, Stack, StackProps, aws_cognito as cognito, aws_iam as iam } from 'aws-cdk-lib'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import path from 'path'; import { BaseResource } from '../base'; export class CognitoResource extends BaseResource { private _cognitoUserPool: cognito.UserPool; constructor(scope: Stack, id: string, props: StackProps) { super(scope, id, props); } setupUserPool(name?: string) { this._cognitoUserPool = new cognito.UserPool( this._scope, `${this._scopeId}-${name ?? 'user-pool'}`, { userPoolName: `${this._scopeId}-${name ?? 'user-pool'}`, passwordPolicy: { minLength: 8, tempPasswordValidity: Duration.days(7), requireDigits: false, requireLowercase: false, requireSymbols: false, requireUppercase: false, }, mfa: cognito.Mfa.REQUIRED, mfaSecondFactor: { otp: true, sms: true, }, accountRecovery: cognito.AccountRecovery.PHONE_AND_EMAIL, autoVerify: { phone: false, email: true, }, selfSignUpEnabled: true, standardAttributes: { phoneNumber: { required: true, }, }, signInAliases: { preferredUsername: true, email: true, phone: true, username: true, }, signInCaseSensitive: false, snsRegion: process.env.AWS_REGION, lambdaTriggers: { createAuthChallenge: this.createNodeJsFn( 'createAuthChallengeFn', 'create-auth-challenge', new iam.Policy(this._scope, `${this._scopeId}-create-auth-challenge-sns-policy`, { statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['SNS:Publish'], resources: ['*'], }), ], }), ), defineAuthChallenge: this.createNodeJsFn( 'defineAuthChallengeFn', 'define-auth-challenge', ), preSignUp: this.createNodeJsFn('preSignUpFn', 'pre-sign-up'), verifyAuthChallengeResponse: this.createNodeJsFn( 'verifyAuthChallengeResponseFn', 'verify-auth-challenge-response', ), }, }, ); return this; } setupAppClient() { this._cognitoUserPool.addClient(`${this._scopeId}-user-pool-app-client`, { userPoolClientName: `${this._scopeId}-user-pool-app-client`, authFlows: { custom: true, userPassword: true, userSrp: true, adminUserPassword: false, }, refreshTokenValidity: Duration.days( parseInt(process.env.REFRESH_TOKEN_DURATION_DAYS || '365', 10), ), idTokenValidity: Duration.days(parseInt(process.env.ID_TOKEN_DURATION_DAYS || '1', 10)), accessTokenValidity: Duration.days( parseInt(process.env.ACCESS_TOKEN_DURATION_DAYS || '1', 10), ), enableTokenRevocation: true, preventUserExistenceErrors: true, }); return this; } setupDomain() { if (!process.env.COGNITO_DOMAIN_PREFIX) { return this; } this._cognitoUserPool.addDomain(`${this._scope}-user-pool-domain`, { cognitoDomain: { domainPrefix: process.env.COGNITO_DOMAIN_PREFIX, }, }); return this; } build() { return this._cognitoUserPool; } private createNodeJsFn(name: string, id: string, role?: iam.Policy) { const fn = new NodejsFunction(this._scope, name, { functionName: `${this._scopeId}-${id}`, runtime: Runtime.NODEJS_14_X, entry: path.join(__dirname, `lambda-function/${id}/index.ts`), }); if (role) fn.role?.attachInlinePolicy(role); return fn; } }
Step 14: Create lambda functions
import { CreateAuthChallengeTriggerEvent } from 'aws-lambda'; import AWS from 'aws-sdk'; function sendSMS(phone: string, message: string) { const params: AWS.SNS.PublishInput = { Message: message, PhoneNumber: phone, }; return new AWS.SNS({ apiVersion: '2010-03-31' }).publish(params).promise(); } export const handler = async (event: CreateAuthChallengeTriggerEvent) => { try { const evtReq = event.request; const evtReqSession = evtReq.session; const phoneNumber = event.request.userAttributes.phone_number; const otp = this.generateOtp(); if (!evtReqSession || evtReqSession.length === 0) { const message = `OTP to login to WebsiteX is ${otp}`; await sendSMS(phoneNumber, message); event.response.privateChallengeParameters = { answer: otp, }; event.response.challengeMetadata = 'CUSTOM_CHALLENGE'; } return event; } catch (error) { Promise.reject(error); } };
import { DefineAuthChallengeTriggerEvent } from 'aws-lambda'; export const handler = async (event: DefineAuthChallengeTriggerEvent) => { const evtReq = event.request; const evtReqSession = evtReq.session; // User is not registered if (evtReq.userNotFound) { event.response.issueTokens = false; event.response.failAuthentication = true; throw new Error('User does not exist', { cause: evtReq, }); } // wrong OTP even After 3 sessions if (evtReqSession.length >= 3 && evtReqSession.slice(-1)[0].challengeResult === false) { event.response.issueTokens = false; event.response.failAuthentication = true; throw new Error('Invalid OTP'); } // Correct OTP! else if (evtReqSession.length > 0 && evtReqSession.slice(-1)[0].challengeResult === true) { event.response.issueTokens = true; event.response.failAuthentication = false; } // not yet received correct OTP else { event.response.issueTokens = false; event.response.failAuthentication = false; event.response.challengeName = 'CUSTOM_CHALLENGE'; } return event; };
export const handler = (event, _, callback) => { // Confirm the user event.response.autoConfirmUser = true; // Set the email as verified if it is in the request if (event.request.userAttributes.hasOwnProperty('email')) { event.response.autoVerifyEmail = true; } // Set the phone number as verified if it is in the request if (event.request.userAttributes.hasOwnProperty('phone_number')) { event.response.autoVerifyPhone = true; } // Return to Amazon Cognito callback(null, event); };
import { VerifyAuthChallengeResponseTriggerEvent } from 'aws-lambda'; export const handler = async (event: VerifyAuthChallengeResponseTriggerEvent) => { if (event.request.privateChallengeParameters.answer === event.request.challengeAnswer) { event.response.answerCorrect = true; } else { event.response.answerCorrect = false; } return event; };
Step 15: Build an AWS CDK application
yarn build
Step 16: Deploy the stack
cdk deploy --profile agapifa
Step 17: Verify on the AWS console
In this tutorial, you learned how to install the AWS CDK, set up and initialize an AWS CDK project, assemble it into a CloudFormation template, and deploy to AWS Cloud. If you want to remove the newly created stack from your AWS account, run the following command.
cdk destroy --profile agapifa
Good luck with your installation!!!
--------------------------------
Reference documents: