Creating secure, serverless webhook targets on AWS

Justin Stewart
Senior Software Engineer
July 29, 2022

How to use Lambda function URLs to quickly get up and running with Formsort in your AWS environment.

If you're on AWS, this article will show you an easy way to set up an authenticated webhook target that can scale using Lambda function URLs. While they don't provide nearly the breadth of the features that come with API Gateway, function URLs are simple and trivial to roll out. Used in conjunction with Formsort's signed webhook requests, we can also make them secure.

We'll primarily build out the code for our Lambda handler in this post. While we use Python, all of this can be ported to the Lambda runtime of your choice.

The Architecture

We chose to have our Lambda publish to an SQS queue for this because they are simple and scaleable consumption targets for other parts of your system. This makes them a great way to get started, because your initial bottleneck will be concurrent Lambda invocations. Default concurrency as of the time of this writing is 1000 invocations per account per region! This gives you great throughput capabilities for inbound answers very early on and can subsequently be increased through a support request with AWS.

The Handler

As a reminder, here are the parts of the event generated from a Lambda function URL that we care about that will be passed to your handler:

{
    "headers": {
        "x-formsort-signature": "fooXvb2luxtOgquxGbazmpYFsvu_VOduxqlEwN0Wbar",
    },
    "body": "{\\"answers\\": {\\"confirm\\": true}, \\"responder_uuid\\": \\"xxxxxxx-2009-4151-a206-80f2739ec3ec\\", \\"variant_label\\": \\"main\\", \\"variant_uuid\\": \\"xxxxxxxxx-82c2-4d94-9e53-1f4c918be198\\", \\"finalized\\": true}"
}

The handler looks like this:

import boto3
import os

SQS_QUEUE_URL = os.getenv("SQS_QUEUE_URL")

def handler(event, context):

    # Send Message to Queue
    sqs = boto3.resource("sqs")
    answers_queue = sqs.Queue(SQS_QUEUE_URL)
    answers_queue.send_message(MessageBody=event["body"])
    return {
        "statusCode": 200,
        "body": "success"
    }

You can easily use this code snippet creating a Lambda function in the console. If you're doing this from scratch, you'll also want to setup an SQS queue and use that Queue URL in the above SQS_QUEUE_URL environment variable. The referenced project in the introduction will provision this queue for you automatically using CloudFormation and expose it as an export.

After your Lambda function URL is up and running, let's test it out with Formsort. I'm going to go into my integrations page for a sample flow and set up a webhook:

webhook-data-submission-frequency

Try sending some test submissions. If you go to your SQS queue afterwards and do some polling, you should see them populate:

SQS-queue-submission-data

If this is looking good - congratulations! You now have a webhook that can scale in AWS for your Formsort answers. This is a great place to start, but next, we'll add signature verification to only allow requests originating from Formsort to be allowed to populate your queue.

Signature Verification

You can reference how you can perform signature verification with Python in our docs here. Here is what it's doing overall:

import base64
import hashlib
import hmac
import os

def hmac_sign(signing_key, original_request_body):
    key = signing_key.encode("utf8")
    message = original_request_body.encode("utf8")
    return (
        base64.urlsafe_b64encode(
            hmac.new(key, message, hashlib.sha256).digest())
            .rstrip(b"=")
            .decode("utf8")
    )

That's going to take in the signature passed in by the x-formsort-signature headers along with the body of the request. Working this into our handler and the signing key pulled from our integration, our code now looks like this:

SQS_QUEUE_URL = os.getenv("SQS_QUEUE_URL")
SIGNING_KEY = os.getenv("SIGNING_KEY", "your-signing-key")

FORBIDDEN = {"statusCode": 401, "body": "forbidden"}
SUCCESSFUL = {"statusCode": 200, "body": "successful"}

def handler(event, context):

    # Verify Signature
    if not event["headers"].get("x-formsort-signature"):
        return FORBIDDEN
    signature = hmac_sign(SIGNING_KEY, event["body"])
    if signature != event["headers"]["x-formsort-signature"]:
        return FORBIDDEN

    # Send Message to Queue
    sqs = boto3.resource("sqs")
    answers_queue = sqs.Queue(SQS_QUEUE_URL)
    answers_queue.send_message(MessageBody=event["body"])
    return SUCCESSFUL

Let's test this out in our Formsort integration again. If we disable signing, it should fail:

webhook-lambda-url-error

If we enable signing, it should pass:

webhook-lambda-url-successful

Conclusion

AWS Lambda function URLs are a great way to get started using Formsort webhooks if you're on AWS. They give a simple, scaleable method to consume responder answers, while providing a ton of flexibility for downstream processing.

Want to learn more? Check out our docs or chat with our team.