Create a Cognito backed Rest API with CDK

In this post, you learn how to use AWS CDK to create a REST API which is secured with AWS Cognito in AWS. The components used for this application are, AWS Cognito our authentication takes place, AWS API Gateway which receives http requests as an entrance to our application, AWS Lambda where we implement our business logic, Dynamodb a NoSQL Database where we store our data.

Firstly, let’s initialize a CDK typescript project: cdk init app –language=typescript.

Then we go to package.json to declare the dependencies for our project. Add the following modules to the dependencies:

"@aws-cdk/core": "*",
"@aws-cdk/aws-apigateway": "*",
"@aws-cdk/aws-cognito": "*",
"@aws-cdk/aws-dynamodb": "*",
"@aws-cdk/aws-lambda": "*",

Then run “npm install” in the root directory of your project. This will download the dependencies and give you intellisense which comes very handy. You can also avoid adding the dependencies in the package.json file and run npm install “package x” command to install them one by one. The go to the lib folder and the typescript file for your application stack.

Add the following import statements to before class declaration:

import * as cdk from ‘@aws-cdk/core’; import { LambdaRestApi, CfnAuthorizer, LambdaIntegration, AuthorizationType } from ‘@aws-cdk/aws-apigateway’; import { UserPool } from ‘@aws-cdk/aws-cognito’ import apigateway = require(“@aws-cdk/aws-apigateway”); import dynamodb = require(“@aws-cdk/aws-dynamodb”); import lambda = require(“@aws-cdk/aws-lambda”); import { RemovalPolicy } from “@aws-cdk/core”;

In order to create our database, add the following:

const dynamoTable=new dynamodb.Table(this,'mytestDB',{
  partitionKey:{
    name:'itemId',
    type:dynamodb.AttributeType.STRING
  },
  removalPolicy:RemovalPolicy.SNAPSHOT
});

Here we defined a partition key ‘itemId’ of type string, and the removal policy of our database. As opposed to the previous demo, I opted for “SNAPSHOT” removal policy. With this policy, AWS takes a snapshot of your database before someone removes it, which will give you the option to recreate the db in case you need it. I don’t make a big change to the previous lambda functions. Here we just add a get all function to returns everything in the database.

const getAll = new lambda.Function(this, 'getAllitems',{
  code: new lambda.AssetCode('./src'),
  handler:'get-all.handler',
  runtime:lambda.Runtime.NODEJS_10_X,
  environment:{
    TABLE_NAME: dynamoTable.tableName,
    PRIMARY_KEY:'itemId'
  }
});

As of now, our lambdas cannot connect to the database, as the IAM role for them is not yet created. Using the following command, we grant access to our lambdas, while respecting the prinicple of least privilege.

dynamoTable.grantReadData(getAll);

Having created our persistance layer and our business logic layer, we go ahead and create the API to our application. Using the following code you can spin up an API Gateway with your lambda already integrated with it:

const getItemsLambdaRestApi = new LambdaRestApi(this, 'getItemsLambdaRestApi', {
  restApiName: 'get all items test api',
  handler: getAll,
  proxy: false,
});

Then we create a new userpool. For the signInAliases you can choose email, phone and username.

const userPool = new UserPool(this, 'userPool', {
  signInAliases: {
    email: true
  }
});

Finaly we create an authorizer and hook it up to our Lambda/API Gateway get method.

const authorizer = new CfnAuthorizer(this, 'cfnAuth', {
  restApiId: getItemsLambdaRestApi.restApiId,
  name: 'simpleTestAPIAuthorizer',
  type: 'COGNITO_USER_POOLS',
  identitySource: 'method.request.header.Authorization',
  providerArns: [userPool.userPoolArn],
});

const getResource = getItemsLambdaRestApi.root.addResource('getitems');

getResource.addMethod('GET', new LambdaIntegration(getAll), {
  authorizationType: AuthorizationType.COGNITO,
  authorizer: {
    authorizerId: authorizer.ref
  }
});

Here is our get all lambda function. Add a file called get-all.ts and add the following to it.

const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || '';

export const handler = async () : Promise <any> => {

  const params = {
    TableName: TABLE_NAME
  };

  try {
    const response = await db.scan(params).promise();
    return { statusCode: 200, body: JSON.stringify(response.Items) };
  } catch (dbError) {
    return { statusCode: 500, body: JSON.stringify(dbError)};
  }
};

Now we are ready to verify our cdk stack. First run ‘npm run build’ which compiles typescript to javascript. Then run cdk synth. This should give a cloud formation template. If it returns an error, go to previous steps and verify your work. Before you can deploy this applicaion, you need to bootsrap your environment. To do that, run the following.

cdk bootstrap aws://youraccountID/yourregion

set CDK_NEW_BOOTSTRAP=1
cdk bootstrap aws://youraccountID/yourregion

One more thing before deploying, in the bin folder, if you add the following the applicaiton will be deployed using your aws config.

env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

Now you are ready to deploy your cdk app.

npm run build
cdk deploy

Some more useful cdk commands:

cdk ls //lists the stacks in your application
cdk diff //shows the difference of your current deployed stack and your local stack
cdk doctor //*kind of* verifies your cdk stack for warnings

That is it for a very minimal backend in aws. Of course, you can add more functions, to do update and delete. But as they are very similar, they are left out in this demo.

The source code on github: https://github.com/pedramha/cdk-lambda-cognito

This project is maintained by pedramha