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: