How To Build a Secure B2B and B2C Serverless Application with DynamoDB, AWS Lambda API Gateway, and AWS Cognito
Creating an AWS-authenticated serverless solution for B2B and B2C

How To Build a Secure B2B and B2C Serverless Application with DynamoDB, AWS Lambda API Gateway, and AWS Cognito

Hello 👋🏼,

My name is Andrei, I am a Senior #AWS Architect & Developer Consultant / Contractor / Freelancer.

This post will guide you through creating an AWS-authenticated #serverless solution 😎 by implementing AWS #Cognito + AWS #APIGateway + AWS #Lambda (for the processing layer) + AWS #DyanmoDB (for the data storage layer)

As Christmas 🎄 is just 2 months away, the application we'll build is about the Christmas market event. (tiny houses around the city with Christmas theme products).

  • The third party, the local city hall (#B2B) will have access to approve the clients’ Christmas market
  • The clients (#B2C) will create an account and submit their Christmas market for approval.

________________________

⚠️ Prerequisites:

  • An AWS Account: Used to deploy the architecture
  • AWS CLI configured with the AWS Account: Needed by #AWSSAM
  • AWS SAM to deploy the template: We will use it only as a tool to deploy the architecture
  • AWS Level: Intermediate

________________________

🛠 Architecture:

The SAM template is available on the following link but let’s first check the architecture. (I like drawio to "draw" architectures).

No alt text provided for this image

For production, I suggest using one API Gateway and Cognito for B2C and another for B2B.

  1. The API Gateway will have the APIs definitions through #openapi swagger file available inside cloudformation -> API gateway module and will be integrated with Cognito to implement authenticated APIs (B2C & B2B)

- /v1/b2b/christmas-markets/city/{city}:GET

- /v1/b2b/christmas-markets/{marketId}/user/{userId}:PATCH

- /v1/b2c/christmas-markets/user/{userId}:GET

- /v1/b2c/christmas-markets/user/{userId}:POST

- /v1/b2c/christmas-markets/{marketId}/user/{userId}:PATCH

- /v1/b2c/christmas-markets/{marketId}/user/{userId}:DELETE

2. Cognito user pool with B2C & B2B clients

3. The lambda function will be used to handle all the APIs through a dispatching logic.

4. #IAMRole will be used by the lambda to send logs to #cloudwatch & execute CRUD operations on the DynamoDB Table

5. DynamoDB table, perfect for serverless architectures thanks to its scalability and availability, as long as your data entities are not connected (no JOINs).

Table characteristics:

Partition & Sort Key: userId & marketId

GSI (Global Secondary Index): city

Item structure:

{
  userId: string;
  marketId: string;
  marketName: string;
  marketDescription: string;
  marketAveragePrice: number;
  marketApproved: boolean;
  city: string;
}        

________________________

🚀 Deploying the architecture:

To successfully deploy the architecture, after you have cloned the repository and installed the AWS CLI & AWS SAM, we need to configure access & secrets keys.

You can create an IAM User with AdministratorAccess managed policy, (only for this guide)

No alt text provided for this image
No alt text provided for this image

Save the access keys inside the AWS credentials files: ~/.aws/credentials

[default]
aws_access_key_id=<token>
aws_secret_access_key=<token>        

If you specify a different value than the default, add the key --profile [profile_name] when executing the deployment command.

If you want to update the architecture names in order to avoid any duplicate errors, check the main.yaml of the cloudformation template

Once you have configured the credentials, open the terminal and run the command to deploy the architecture. Add the “—guided” key the first time you run the template.

sam build -t cloudformation/main.yaml && sam deploy --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND --guided        
No alt text provided for this image
No alt text provided for this image
No alt text provided for this image

Congratulations! You have released a completely #serverless architecture!

Time to have a one-minute party! 🥳

No alt text provided for this image

________________________

🔀 Application flow:

As specified above, one lambda function handles all the APIs of this feature.

I highly recommend using one lambda for all the CRUD operations of the feature.

As the lambda is connected to the API Gateway as a Proxy Integration, the lambda receives inside the event a lot of information, the most important for our use case:

resource/path

  • HTTP method
  • path parameters
  • body

No alt text provided for this image

As the application is not the most important part of the guide, I have used plain javascript.

I will post soon a separate guide, with the same architecture but using Typescript instead of plain javascript

________________________

👨🏻💻 Lambda code:

All the processing flow is executed inside the handler.

By using a switch constructor of the resource path & HTTP verb, dynamoDB methods are executed based on the API.

const uuid = require('uuid')
const AWS = require('aws-sdk');
exports.handler = async (event) => {


    const dynamoDBName = process.env.DynamoDBTableName;
    const dynamoDBClient =  new AWS.DynamoDB.DocumentClient({
        apiVersion: "2012-08-10"
    });
    console.info(event);
    const transformedEvent = {
        resource: event.resource,
        httpMethod: event.httpMethod,
        pathParameters: event.pathParameters,
        body: event.body !== null ? JSON.parse(event.body) : null
    }
    const api = transformedEvent.resource+':'+transformedEvent.httpMethod;
    console.info('api: '+api);
    // dispatch logic
    switch(api) {
        case '/v1/b2b/christmas-markets/city/{city}:GET': {
            const params = {
                TableName: dynamoDBName,
                IndexName: "city",
                KeyConditionExpression: "city = :city",
                ExpressionAttributeValues: {
                    ':city': transformedEvent.pathParameters.city
                }
            }


            try {
                const item = await dynamoDBClient.query(params).promise();
                return {
                   body: JSON.stringify(item.Items),
                   statusCode: 200
                };
            } catch (error) {
                throw new Error('DynamoDB Processing Failed: '+ JSON.stringify(error));
            }
        }
        case '/v1/b2b/christmas-markets/{marketId}/user/{userId}:PATCH': {
            const params = {
                TableName: dynamoDBName,
                Key: {
                    userId: transformedEvent.pathParameters.userId,
                    marketId: transformedEvent.pathParameters.marketId,
                },
                ReturnConsumedCapacity: 'TOTAL'
            }


            try {
                const response = await dynamoDBClient.get(params).promise();
                const item = response.Item;
                item.marketApproved = transformedEvent.body.approved;
                await dynamoDBClient.put({
                    TableName: dynamoDBName,
                    Item: item,
                    ReturnConsumedCapacity: 'TOTAL'
                }).promise();
                return {
                    body: JSON.stringify({message: 'success'}),
                    statusCode: 200
                 };
            } catch (error) {
                throw new Error('DynamoDB Processing Failed: '+ JSON.stringify(error));
            }
        }
        case '/v1/b2c/christmas-markets/user/{userId}:GET': {
            const params = {
                TableName: dynamoDBName,
                KeyConditionExpression: '#userId = :userId',
                ExpressionAttributeNames: {
                    '#userId': 'userId'
                },
                ExpressionAttributeValues: {
                    ':userId': transformedEvent.pathParameters.userId
                },
                ReturnConsumedCapacity: 'TOTAL'
            }


            try {
                const item = await dynamoDBClient.query(params).promise();
                return {
                    body: JSON.stringify(item.Items),
                    statusCode: 200
                 };
            } catch (error) {
                throw new Error('DynamoDB Processing Failed: '+ JSON.stringify(error));
            }
        }
        case '/v1/b2c/christmas-markets/user/{userId}:POST': {
            const item = {
                marketApproved: false,
                userId: event.pathParameters.userId,
                marketId: uuid.v4(),
                marketName: transformedEvent.body.marketName,
                marketDescription: transformedEvent.body.marketDescription,
                marketAveragePrice: transformedEvent.body.marketAveragePrice,
                city: transformedEvent.body.city
            }
            const params = {
                TableName: dynamoDBName,
                Item: item,
                ReturnConsumedCapacity: 'TOTAL'
            }
    
            await dynamoDBClient.put(params).promise();
            return {
                body: JSON.stringify({message: 'success'}),
                statusCode: 200
             };
        }
        case '/v1/b2c/christmas-markets/{marketId}/user/{userId}:PATCH': {
            const params = {
                TableName: dynamoDBName,
                Key: {
                    userId: transformedEvent.pathParameters.userId,
                    marketId: transformedEvent.pathParameters.marketId,
                },
                ReturnConsumedCapacity: 'TOTAL'
            }


            try {
                const response = await dynamoDBClient.get(params).promise();
                const item = response.Item;
                item.marketName = transformedEvent.body.marketName;
                item.marketDescription = transformedEvent.body.marketDescription;
                item.marketAveragePrice = transformedEvent.body.marketAveragePrice;
                item.marketApproved = false;
                await dynamoDBClient.put({
                    TableName: dynamoDBName,
                    Item: item,
                    ReturnConsumedCapacity: 'TOTAL'
                }).promise();
                return {
                    body: JSON.stringify({message: 'success'}),
                    statusCode: 200
                };
            } catch (error) {
                throw new Error('DynamoDB Processing Failed: '+ JSON.stringify(error));
            }
        }
        case '/v1/b2c/christmas-markets/{marketId}/user/{userId}:DELETE': {
            const params = {
                TableName: dynamoDBName,
                Key: {
                    userId: transformedEvent.pathParameters.userId,
                    marketId: transformedEvent.pathParameters.marketId
                },
                ReturnConsumedCapacity: 'TOTAL'
            }


            try {
                await dynamoDBClient.delete(params).promise();
                return {
                    body: JSON.stringify({message: 'success'}),
                    statusCode: 200
                 };
            } catch (error) {
                throw new Error('DynamoDB Processing Failed: '+ JSON.stringify(error));
            }
        }
        default:
            throw new Error('Method not implemented')
    }
};







;        


________________________

🧪 Testing the APIs:

APIs can be exported and imported inside #Postman for testing.

No alt text provided for this image


A small frontend app will be available soon in the next guide 😊

The B2C client token can be retrieved by using the Launch Hosted UI.

No alt text provided for this image


The B2B client can be retrieved from the oauth2 endpoint: https://[domain].auth.eu-west-1.amazoncognito.com/oauth2/token

curl --location --request POST 'https://[domain].auth.eu-west 1.amazoncognito.com/oauth2/token' 
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'grant_type: client_credentials' \
--header 'scope: B2BIdentifier/ApproveChristmasMarkets' \
--header 'Authorization: Basic clientId+CLIENTsecret' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=[clientId]' \
--data-urlencode 'scope=B2BIdentifier/ApproveChristmasMarkets'\        

The B2B token is used only for the B2B endpoints while the B2C token is used only for the B2C endpoints.

________________________

🧼 Clean the environment:

In order to completely remove the architecture, you can delete the #Cloudformation template from Cloudformation and #S3 bucket used to upload the wSAM build.


_______________________

That’s it! A simple AWS serverless authenticated application! 🎉

I hope I could help you through the journey into the AWS world. 😎

This is my first article and I wanted to share with you information that is useful like building a serverless enterprise solution - an early Christmas gift 🎅🏻.

In the following weeks, I will post new articles (based on application design) using this architecture and application flow.


If you need any help or have questions, feel free to drop me a message! 📩

Cristian Savin

🌍 Senior Java Backend Developer • Contractor • Freelancer | Implementing clean, maintainable and scalable web apps

3y

Great article Andrei!! 🏆🔥

To view or add a comment, sign in

More articles by Andrei Marius Diaconovici

  • How to create NodeJS custom libraries

    Hello 👋🏼, My name is Andrei, I am a Senior NodeJS Developer & #AWS Architect Consultant / Contractor / Freelancer…

    1 Comment
  • Deploy Lambda REST API with Deno 🦕

    Hello 👋🏼, My name is Andrei, I am a Senior NodeJS Developer & AWS Architect Consultant / Contractor / Freelancer…

    3 Comments
  • How To Build Clean NodeJS REST API with AWS Lambda

    Hello 👋🏼, My name is Andrei, I am a Senior NodeJS Developer & AWS Architect Consultant / Contractor / Freelancer…

    2 Comments

Others also viewed

Explore content categories