The Indian government announced the Open API Service from Aarogya Setu (ASetu) in August, 2020. It enables organizations and business entities to fetch the COVID-19 risk status of their employees or any other ASetu app users. Most office spaces have a visitor management system that controls access or logs the identities of people entering the office. When you integrate with ASetu, this system can become COVID-aware and assess the COVID-19 risk status of a visitor before allowing them in.

This post walks you through an example application that can fetch a person’s COVID-19 risk status from ASetu. It can act as reference of how to integrate your own visitor management system with ASetu using AWS services.

The solution is made up of only serverless components that require minimal maintenance and are secure. It’s built using AWS Cloud Development Kit (CDK), an infrastructures as code (IaC) library that makes it easy to deploy infrastructure or integrate with existing infrastructure.

Legal notice

AWS does not hold a registration for use of the Aarogya Setu Open API. In the event that you wish to use the APIs along with AWS services, you as the customer will be responsible for registration on the Aarogya Setu Open API Service Portal (note: this link may not work from certain locations) and for ensuring that your use of the APIs within the AWS services is in compliance with the applicable conditions. AWS is not responsible for ensuring compliance with the conditions for use of the Open APIs.

Architecture overview

The following diagram shows the architecture of the example application we deploy in this post. It’s made up of only serverless components, to create a solution that requires minimal maintenance and is secure. You can find the code for this demo hosted in the GitHub repo.

Solution architecture

The user interface is a web application built using NextJS, a ReactJS variant, and it uses the AWS Amplify library to seamlessly integrate with Amazon Cognito. Amazon Cognito automatically manages authentication and access to the web application. It also integrates with Amazon API Gateway, so that only authorized users can make API calls. An Amazon Simple Storage Service (Amazon S3) bucket stores the static files for the web application, and it’s distributed through an Amazon CloudFront web distribution.

In the backend, AWS Lambda functions process the API calls and interact with ASetu to fetch COVID-19 risk status. The secrets for interacting with ASetu are securely stored with AWS Secrets Manager and are accessed only by Lambda functions during an invocation. The results of a request are stored in Amazon DynamoDB tables.

Application overview

The demo application performs the following actions of a visitor management system:

  • Request COVID-19 risk status for a single user
  • Queue requests for multiple users through a bulk request API
  • Retrieve risk status logged for multiple users and show the status in a table
COVID status page

COVID status page

Our use case shows just one way to consume the API for a simple visitor management system. Other systems can consume the API, and you can modify the architecture for deeper integration.

Prerequisites

These are the prerequisites for deploying this solution:

  • Set up AWS CDK for your environment. Make sure to set up the environment for a Python based AWS CDK app.
  • Register for a developer account on the ASetu portal (note: this link may not work from certain locations). You can only query the status for the mobile number used to register the developer account.
  • Download the code from the GitHub repo.

Deploying the solution

Download or clone the repo and open a terminal inside it. Then complete the following steps to deploy your application. See the following code:

python3 -m venv .env
source .env/bin/activate
pip install -r requirements.txt
cdk deploy asetuapi
# TODO: Fill the placeholders in `secrets.json`
python update-api-secret.py
cdk deploy asetuapifrontend

These commands perform operations equivalent to the following steps. You can enter the commands directly or make the changes yourself.

  1. Clone or download this repository to a directory and open it in a terminal.
  2. Create a new virtual environment with python3 -m venv .env to install dependencies. This creates an .env
  3. Activate the environment.
    1. If you’re on Mac or Linux, use source .env/bin/activate.
    2. If you’re on Windows, use .env\Scripts\activate.bat.
  4. Install the required libraries using pip install -r requirements.txt.
  5. Deploy the backend using cdk deploy asetuapi. This command bundles up the dependencies and deploys the backend infrastructure for the application. The AWS CDK app also creates a secret in the Secrets Manager, but it doesn’t put the API secret values in it.
  1. Fill the placeholders in json with the values from your Aarogya Setu Open API account and then run python put-api-secret-value.py. This puts the values from secrets.json into Secrets Manager.
  1. Deploy the front end using cdk deploy asetuapifrontend. It packages the front-end application for export and deploys the infrastructure.
  1. Open appurl to access the web page.
  2. Sign up as a new user and then log in.

Login page

You can now check COVID-19 risk status and make your office safe for everyone.

Application home page

Automating the infrastructure

This solution is written in Python as an AWS CDK app. This makes it extremely flexible and can be easily modified or integrated with existing systems. The app is made of two stacks that are broadly the backend and front-end infrastructure components. It also has three helper scripts containing custom logic for gluing the two stacks. In this section, we break down some components of this solution.

AWS CDK constructs

Amazon Cognito handles the core login, signup, and authentication functionality. It integrates with the Amplify framework and API Gateway to allow only logged in and authorized users to make API requests. The following code creates the Amazon Cognito user pool and client:

        user_pool = cognito.UserPool(
            self,
            "AppUserPool",
            self_sign_up_enabled=True,
            account_recovery=cognito.AccountRecovery.PHONE_AND_EMAIL,
            user_verification=cognito.VerificationEmailStyle.CODE,
            auto_verify={"email": True},
            standard_attributes={"email": {"required": True, "mutable": True}},
        )
        user_pool_client = cognito.UserPoolClient(
            self, "UserPoolClient", user_pool=user_pool
        )

You can reconfigure this user pool to disable self-signup so only user accounts created by the account admin can access authenticate with the user pool. For more information about the user pool parameters, see class UserPool (construct).

To configure the API Gateway, we have to drop into the L1 constructs layer of AWS CDK, which have a 1:1 mapping with AWS CloudFormation resources. The following code is a modified version of the code used in the example application, which adds the two necessary parameters to the AWS CDK construct and configures them with Amazon Cognito values:

        scan_table_integration = apigw.LambdaIntegration(scan_table, proxy=True)
        scan_table_resource = api.root.add_resource("scan")
        scan_method = scan_table_resource.add_method(
            "GET",
            scan_table_integration,
            api_key_required=False,
            authorizer=auth,
            authorization_type=apigw.AuthorizationType.COGNITO,
        )         # Override authorizer to use COGNITO to authorize apis
        single_method.node.find_child("Resource").add_property_override(
                "AuthorizationType", "COGNITO_USER_POOLS"
            )
        single_method.node.find_child("Resource").add_property_override(
                "AuthorizerId", {"Ref": auth.logical_id}
            )

AWS CDK constructs also have helper methods to grant permissions to various service roles. For example, enforcing least privilege access for Lambda functions becomes as easy as calling a method. The following code gives the single user request handler read access to API secrets, and read and write access to DynamoDB tables:

        user_status_table.grant_read_write_data(single_request)
        requests_table.grant_read_write_data(single_request)
        api_secret.grant_read(single_request)

For the web distribution, we follow best practices to ensure that the S3 bucket remains private and only the CloudFront distribution is granted access to it. See the following code:


        oai = cloudfront.OriginAccessIdentity(self, "OAI")
        cfd = cloudfront.CloudFrontWebDistribution(
            self,
            "ReactAppDistribution",
            origin_configs=[
                {
                    "s3OriginSource": {
                        "s3BucketSource": bucket,
                        "originAccessIdentity": oai,
                    },
                    "behaviors": [cloudfront.Behavior(is_default_behavior=True)],
                }
            ],
        )
        # only allows cloudfront distribution to read from bucket
        bucket.grant_read(oai.grant_principal)

Integrating Amplify

We use the Amplify framework to seamlessly integrate our web app with Amazon Cognito user pools. The Amplify library handles login and sign-in. It also makes it possible to authorize only the logged-in users to make API calls to API Gateway. The following code from index.js shows where Amplify is configured inside the existing React logic:

import Amplify, { Auth } from "aws-amplify";<br />import awsconfig from "../aws-exports";<br />import { withAuthenticator } from "@aws-amplify/ui-react";<br /><br />Amplify.configure(awsconfig);<br /><br /><strong>function</strong> Home() { // logic<br />//<br />}<br /><br />function HomeExport() {<br />  return (<br />    &lt;ToastProimport Amplify, { Auth } from "aws-amplify";
import awsconfig from "../aws-exports";
import { withAuthenticator } from "@aws-amplify/ui-react"; Amplify.configure(awsconfig); function Home() { // logic
//
} function HomeExport() {
  return (
    <ToastProvider>
      <Home />
    </ToastProvider>
  );
} export default withAuthenticator(HomeExport);

The Amplify library needs certain configuration parameters to integrate with Amazon Cognito. These values are stored in aws-exports.js. These values need to be filled when building and bundling the web app before deployment. However, we can only get these values after deploying the backend stack, so we need some custom logic to glue the backend and front-end stacks together. See the following code:

const awsmobile = {
  aws_project_region: "REGION",
  aws_cognito_region: "REGION",
  aws_user_pools_id: "USER-POOL-ID",
  aws_user_pools_web_client_id: "WEB-CLIENT-ID",
  aws_api_endpoint: "API-ENDPOINT-URL",
}; export default awsmobile;

We demonstrate how to add this custom logic to the stack in the next section.

Packaging and deploying with custom logic

AWS CDK allows us to add custom logic to the stack definition. In the backend stack, the Lambda functions require a layer that needs to be packed into a .zip file. The logic for installing and zipping the dependencies is written in a function and is called as part of the stack declaration in asetuapi_stack.py. See the following code:

app = core.App()
backend = AsetuapiStack(app, "asetuapi", create_dependency_layer, env=deploy_env)
AsetuapiFrontendStack(app, "asetuapifrontend", generate_exports_and_bundle, env=deploy_env)

Similarly, the front-end web application needs to be built and its static files exported. It also depends on the user pool ID and web client ID of the Amazon Cognito resources deployed by the backend stack. The logic to perform these steps is also scripted in a function and is called as part of the stack declaration in the asetuapifrontend_stack.py.

This bundling of prerequisite logic and stack declaration makes sure that all required resources are available at stack deployment. It also makes it easy for the logic to be modified to fit different CI/CD pipelines.

Walkthrough of control flow

The critical piece of logic that communicates with the ASetu API is implemented in the check_mobile_number function of the get_status.py file. It’s tuned to follow the flow given by the ASetu API documentation. The following diagram illustrates this flow.

Control flow diagram

Control flow diagram take from Aarogya Setu API portal

See the following code:

def check_mobile_number(number):
    """
    Check mobile number for COVID status. It first checks user status table
    for cached entry. If the entry is expired it makes a fresh request and then
    gets status for the request     Parameters
    ----------
    number: str
        User mobile number of the format "+91XXXXXXXXXX"
    """     # reject empty or invalid mobile numbers
    if not (number and valid_mobile_number(number)):
        message = create_return_body(number, "Mobile number is invalid")
        return create_return_response(200, message)     envvar = EnvVar()
    secret = Secret(envvar)     # check if status exists in ddb
    entry = check_user_status(number, envvar)     # returned cached entry if it exists and status is not pending or denied
    if entry is not None and entry["request_status"] == APPROVED:
        message = create_return_body(number, entry["message"], entry["colour"])
        return create_return_response(200, message)     # check ddb for pending request
    entry = get_pending_request(number, envvar)     # create new request if it doesn't exist
    if entry is None:
        token = get_token(secret)         if token is None:
            message = create_return_body(
                number, "Failed to get token from Aarogya Setu. Please try again"
            )
            return create_return_response(502, message)         request_id = create_new_request(number, token, secret)         if request_id is None:
            message = create_return_body(
                number, "Failed to get request id from Aarogya Setu. Please try again"
            )
            return create_return_response(502, message)         store_pending_request(number, token, request_id, envvar)
    else:
        token = entry["token"]
        request_id = entry["request_id"]     content = get_status_content(number, token, request_id, secret)     if content is None:
        message = create_return_body(
            number, "Failed to get status from Aarogya Setu. Please try again"
        )
        return create_return_response(502, message)     # default status
    status = {
        "message": "User as rejected request. Please create a new request",
        "color_code": WHITE,
    }     # store rejected and approved statuses
    if content["request_status"] != PENDING:         if content["request_status"] == APPROVED:
            status = decode_status(content, secret)         store_user_status(number, status, content["request_status"], envvar)
        delete_pending_request(number, envvar)     return_response = create_reponse_from_status(
        number, status, content["request_status"]
    )
    logger.info(return_response)     return return_response

Let’s break this logic down:

  1. The first step is to parse and validate the input data to the event. A mobile number pattern regex is used to confirm if the string matches.
  2. We create an object containing the environment variables. This is then used to create an object containing the secrets that are retrieved from the parameter store.
  3. We check if an existing COVID-19 status exists for the number previously stored in the database. This check also considers if the previous entry has expired. The expiry time can be configured using the USER_STATUS_EXPIRY_DAYS If a valid entry exists, we use it to create a return response.
  4. If a status entry doesn’t exist, we check if a pending request exists for the same number previously stored in the database. This also considers if the request has expired. The expiry can be configured using the PENDING_REQUEST_EXPIRY_HOURS If a valid entry exists, we use it get the COVID-19 status for the number.
  5. If a pending request entry doesn’t exist, we have to create a user status request with the ASetu API:
    1. We first get an authorization token from the /token This is passed as the Authorization header in all subsequent requests to ASetu API.
    2. We then make a status request with the /userstatus This request returns a unique user status request ID, which we use to query for the user status. This request ID is stored as a pending request in the database.
  6. Now that we have a valid request ID, we use it to query /userstatusbyreqid for COVID-19 status. We have three possible responses based on whether the user accepts, declines, or doesn’t do anything:
    1. If the user accepts, the response gives their COVID-19 status. We store this in the database, delete the pending request, and return the COVID-19 status in the Lambda function return value.
    2. If the user declines, we store a default rejected response, delete the pending request, and return the rejected message in the Lambda function return value.
    3. If the user doesn’t take any action, we keep the pending request as is and return a default pending message in the Lambda function return value.

Cleaning up

Now that we’re done deploying and using the application, we delete the deployed services so that we don’t incur unwanted costs. Remove all the stack resources using cdk destroy asetuapi asetuapifrontend.

To delete the S3 bucket used to store static files for the web app, first delete the files in the bucket and then delete the bucket itself.

The CDKToolkit stack also remains deployed in case you want to deploy more AWS CDK applications to that account and Region. You can delete it separately or leave it as it is.

Conclusion

In this post, we introduced a demo application for a COVID-aware visitor management system. We explained its architecture and demonstrated how to use AWS CDK to build and deploy it. We also covered an important piece of logic: the control flow used for communicating with the Aarogya Setu Open API.

We hope you would utilize this sample code to build similar applications for your organization.

kH325 KyChU