Spam-free serverless contact forms using Lambda and SES with Google reCAPTCHA

Post image
Photo by Daria Nepriakhina on Unsplash

Implementing contact forms in popular Content Management Systems (CMS) like Wordpress is a breeze, you download a form plugin, setup your SMTP and you’re done. Wordpress and Wix are nice and gets the job done, but I’m not really a fan of server-based CMSes or anything that requires you to natively run it in a VPS/VM. It’s expensive and it’s slow (fight me!).

There are tons of headless form API services out there like formspree.io and cognito forms, but that’s 8 and 15 dollars a month respectively, to provide a contact form for a landing page that nobody’s ever going to read anyway (hopefully somebody does but well..). I’m cheap and a basic plan for Netflix in the Philippines is a little short of 5 dollars, so Netflix wins over formspree.

This leads me to creating my own contact form with my favorite AWS service - AWS Lambda. We will be implementing the architecture below:

alter-text
Final architecture

Note

Prerequisites:

  • Access to Lambda, SES and IAM to delegate permissions to Lambda
  • An Amazon SES Verified Identity (can be a verified regular email, or custom domain)
  • Account for Google reCAPTCHA

Google reCAPTCHA

Now, before everything else, we have to remember that we are exposing an auth-less public service that is not in anyway excused from spam and abuse. Spam requests to our Lambda function may potentially cost us money from useless/fraudulent invocations, furthermore we will be utilizing another service - Amazon SES which is a separate cost concern.

We can protect our form by utilizing CAPTCHAs - which act as cushion from service abuse by providing challenges to differentiate bots/automated scripts from legitimate humans (or aliens) trying to make first contact.

To do this, We’ll head over to Google reCAPTCHA (note that this is a free service - i like free stuff) and register our site. I used reCAPTCHAv2 which uses challenge-based validation, rather than the score-based v3. For comparison check out this link.

alter-text
Registering our site with reCAPTCHA

Then we need to grab our site key and secret key and note it somewhere, as we will use this for our static html form and Lambda function later on.

alter-text
Copy the site key and secret key

Our “headless” form API

Previously, you would only be able to execute AWS Lambda through event sources like SNS and EventBridge. Exposing Lambda as an HTTP/S endpoint typically would require you to set it up behind an Application Load Balancer (which slaps you with a 20 USD/month minimum) or API Gateway which tops you with unnecessary additional complexity. However, in early 2022 last year, AWS released Function URLs - which allow you to expose HTTPS endpoints for Lambda functions.

rcordial/lambda_contact_form_ses - GitHub

We’ll create a Lambda function from the prepared code above, but we need to specify in the Advanced Settings section that we want to use function URLs.

alter-text
Enabling lambda function URL

This will give us an AWS-generated public endpoint by which we can invoke our function.

alter-text
The actual function URL

Before we deploy our Lambda application, we need to populate some required values in here first. You may ought to use environment variables if you want, but for simplicity I put the values directly inside my application:

6    const toAddress = '<--recipient-address-->';
7    const fromAddress = '<--sender-address-->';
8    const secret_key = '<--reCAPTCHAv2 secret key-->';
9    const region = '<--region-->';
  • toAddress - where we want to send notifications to, can be your verified identity / personal gmail account
  • fromAddress - what identity will SES use to send the email in behalf of
  • secret_key - the reCAPTCHAv2 secret key
  • region - AWS region where Lambda and SES resides

Info

Note that we cannot just simply send an email via SES to anyone while in sandbox mode. We are only allowed to send mails to verified identities that we own. To send outgoing mails for production, AWS requires you to move out and get production access .

We’ll do npm install and zip up our lambda application, or simply run sh build.sh to automatically package the whole thing.

After uploading our code and creating a deployment, we’ll then need to configure a few things in the function URL settings:

  • Configure cross-region resource sharing (CORS)
  • Allow origin - we need to set this to our domain or ‘*’ if we want to test it anywhere
  • Allow headers - not really required, but I allowed content-type here so I can choose to use application/json content type when passing the payload
  • Allowed methods - we set this simply to POST

Now we just need to note the actual function URL, as we will put this later as an action in the static form.

Static Contact Form

I prepared a sample contact form below. By default, the form accepts a name, email address and a message, you can customize it and modify the Lambda function to accept more fields. I plugged TailwindCSS as well to give it a look.

rcordial/static_contact_form - GitHub

We need to replace a few values in the form.

  • <--action--url--> - the generated lambda function URL
  • <--reCAPTCHA v2 site key--> - registered google reCAPTCHAv2 site key we noted earlier

After that we just need to deploy our contact form somewhere in our static site and voila! We’ve successfully implemented a serverless contact form.

Trying it out

So I tried sending myself an email from Ellie in the Last of Us (great series btw.).

alter-text
Trying out a test email

I used a verified identity domain type (reiland.dev) and one of my Protonmail aliases as a recipient.

alter-text
Email received in mailbox

Summary

  1. Created a reCAPTCHA enabled static contact form
  2. Deployed a Lambda function to verify the reCAPTCHA and handle form data
  3. Automated email notifications through Amazon SES invoked from Lambda
  4. It’s serverless again, no bill, no idle charges

You May Also Like