AWS contact form

As mentioned in my last post, Sneddo.net is now a static HTML website hosted on AWS. As part of the migration I wanted to keep the ability for people to contact me via a form on my website. Rather than shipping this off to another product, I thought this could be a good excuse to play with a few more services on AWS.

My requirements we as follows:

  • Have some sort of CAPTCHA to reduce spam
  • Expandable to support multiple sites (as it will used for other sites I host as well)
  • Use native AWS services i.e. avoid having to run an EC2 instance for some form of server-side processing

The solution looks pretty simple on paper: we have an API Gateway, which uses a Lambda function to verify CAPTCHA, and then SES to finally send the email.

On top of this, I’m using Google reCAPTCHA to provide some spam protection.

Setting up reCAPTCHA

I won’t go into too detail in this step as it is pretty straight-forward. Sign up for reCAPTCHA and register a site. Once complete, take note of the Site key and Secret key as we will use these in our Lambda function to verify the CAPTCHA response.

Setting up SES

Again, this step is pretty straight-forward so I won’t spend too much time on this. Add and verify your domain in SES and you’re pretty much ready to go. Unless you plan on using SES to send large volumes of emails, you won’t need to take it out of sandbox mode.

If you get stuck, the documentation is quite easy to follow.

Set up the Lambda function

Next, we create the function that does all the work. I’ve uploaded a copy of my function to Github«LINK TO GITHUB». The script uses the AWS API and node-recaptcha-v2 modules to simplify handling the reCAPTCH verification and email sending.

You will need to modify the sites array to define the Site key and Secret key and email address for your site(s). Once you have done this, zip up the index.js and node_modules folder.

When you create the Lambda function, give it a name (e.g. ContactForm), and select Node.js 4.3 as the runtime. Upload the zip file you just created. Most other fields can be left as defaults, however it may be worth bumping the timeout value up a little bit (in my testing 3 seconds was usually enough, but I bumped up to 10 seconds to be sure).

Setup the IAM role

It is best to create an IAM role for your function with the ability to send emails from SES. Ideally, you should also give the role permission to create and write to logs. You could use this policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:DescribeLogGroups",
                "logs:DescribeLogStreams",
                "logs:PutLogEvents",
                "logs:GetLogEvents",
                "logs:FilterLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

The main line in this policy is the first action “ses:SendEmail”. Without this, the entire function won’t work.

Create the API Gateway

Next, we need to have a gateway to call the Lambda function. Create a new API and then create a method on the API.

Select Lambda Function as the Integration Type, select the region that your Lambda function was created in, and then select the function.

Map client IP address for reCAPTCHA

Although providing the client IP is optional in reCAPTCHA verification, I decided to take the extra step to send this data with the verification. By default the API gateway does not pass the client IP to Lambda, and requires a little extra work.

Select the resource method from the API Gateway console (APIs > your API name > Resources > contact form resource > POST). Here, you are able to configure all 4 stages of the request lifecyle - Method Request, Integration Request, Integration Response, and Method Response. For the IP address mappings, we need to configure the Integration Request

In the Integration Response settings, add a new Body Mapping Template of type application/json. The entire contents of the mapping template should look like this:

{
    "sourceip" : "$context.identity.sourceIp",
    "grecaptcha" : $input.json('$.g-recaptcha-response'),
    "site": $input.json('$.site'),
    "name": $input.json('$.name'),
    "email": $input.json('$.email'),
    "message": $input.json('$.message')
}

I have included all of the fields that are sent to this endpoint, so if you have additional fields on your form you must include them here.

Enable Cross Origin Resource Sharing rules (CORS)

The URL to call your new API endpoint will look something like this: https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/stage. Obviously, this is probably not the the same domain that you are hosting your site on. As a result, any calls to that endpoint will probably be blocked due to Cross Origin Resource Sharing rules (CORS).

You will need to enable CORS on the resource in the API Gateway console by first selecting the resource for the contact form function. Then, from the “Actions” menu, select “Enable CORS”. You will then want to add your base URL in the “Access-Control-Allow-Origin” field, making sure you surround the URL with single quotes. For the sake of security, DO NOT leave the “Access-Control-Allow-Origin” field as the default value ‘*’.

Once you have enabled CORS, a new OPTIONS method will be generated on the resource. You don’t need to do anything with this method, it is only used in CORS requests.

Deploy the API

Once you have created and configured the API, you will need to deploy it so that you can call it. Select the API from the Gateway console, then from the “Actions” menu, select “Deploy API”. Select the appropriate stage (“prod” is a good choice), add an optional description, and deploy.

Create the contact form

To call the API, we can generate a Javascript SDK to simplify the request.

From the API Gateway console, select your new API > Stages > the stage you deployed to, then select the “SDK Generation” tab. Generate a Javascript SDK and download the zip file. Extract the zip to a location on your webserver (e.g. mine is under /assets/js/).

Include the Javascript files in the <head> section of your web pages, as well as the the reCAPTCHA script.

Finally, you will need to add a function to submit your form. You can have a look at my contact form for a more detailed example, but effectively it boiled down to something like the following:

var apigClient = apigClientFactory.newClient({
});

var d = $('#contact-form').serializeArray().reduce(function(m,o){ m[o.name] = o.value; return m;}, {});
apigClient.contactPost({}, d, {})
.then(function(result){
    // ...

    // Code to show success (hidden)

    // ...

}).catch( function(result){
    grecaptcha.reset();
    // ...

    // Code to show error (hidden)

    // ...

});

Final words

So there we have it, a “simple” contact form handler! Building this was a great challenge as there was a lot of content out there that did parts of this, but not much that I could find that were designed with CAPTCHA and support for multiple sites. I’m sure there are probably some mistakes in this as it has been compiled well after the fact, so let me know if you get stuck.

A few tweaks that you could make:

  • Use API keys to further lock down access. I actually do this on my site, though CORS should really be enough…
  • Use environment variables in the Lambda function to track site information. That way no modifications to the code is required.
  • You could probably simplify the submit of the form- the API SDK generates a lot of code, which could probably be trimmed down a bit…