reCaptcha Form with an Amazon AWS serverless backend environment

Marc Logemann
AWS Factory
Published in
10 min readJul 25, 2018

This article is outdated. Please go to https://medium.com/aws-factory/recaptcha-form-with-an-amazon-aws-backend-based-on-cdk-78377db58d1f to see the latest revision on this topic.

In this article i will show you how to implement a website form with Google’s reCaptcha and an Amazon serverless backend environment consisting of AWS API Gateway, AWS Lambda and AWS SNS. The last part, the Simple Notifiction Service does the email sending of the form but you could do various other actions on top of it. The beauty of SNS is that you can add more subscribers as you like and make this form pretty powerful without hacking too much. This article is a slimmed down version of something we have done for a customer but with more subscribers on the Amazon SNS Topic.

The Frontend

The first part is to get reCaptcha into your website form. Believe me, without spam prevention, it doesnt take much time after you encounter the first bots spamming you, so reCaptcha is really a must.

Go to: https://www.google.com/recaptcha/intro/v3beta.html and create your first reCaptcha. You just need to enter a project name (label) and select the type of reCaptcha. We will use “reCaptcha Version2” for this article. Then you must list the domains where this reCaptcha is hosted. Normally you put in your website domain name. If you want to test it locally, i recommend also putting “localhost” in there.

When you are done registering, you get some website key, which is part of your DIV which you will insert into the HTML page. You also get a secret key which is used for backend verification. We will get to that later on. You can just follow Google documentation at this point, which means you should put the

<script src=’https://www.google.com/recaptcha/api.js'></script> 

in the head section of your website and the

<div class=”g-recaptcha” data-sitekey=”xxxxxxxxxxxxxxxxxxx”></div> 

somewhere down below your form just before your submit button. With that, your frontend work should be mostly done. In our case, we want to have the submit button only enabled when successfully passing the spam test though. This requires some more code which is shown below.

Instead of the default DIV, we provide a callback like so and be sure that the default state of the submit button is disabled:


<div class=”g-recaptcha” data-sitekey=”xxxxxxxxxxxxxxxxxxxxxx” data-callback=”ccb”></div>
<button type=”submit” disabled class=”btn btn-three pull-right”>SEND FORM</button>

The Lambda i provided in this article assumes the following form field names: FULLNAME, EMAIL, PHONE, SUBJECT and MESSAGE. If you use different names or more fields, please adapt the javacript function accordingly.

Then we create a javascript function with the name mentioned in the value part of the callback parameter which is ccb:

<script>
function ccb(response) {
$(‘:input[type="submit"]’).prop('disabled', false);
}
</script>

Of course you would most likely do a lot more stuff like validate user input or whatnot. But for the sake of simplicity we just make those little additions. You might wonder which action we supply for our form but the exact URL is not known yet, so we get back to that as soon as we have AWS Lambda and API Gateway set up.

Remember: Doing webforms without spam protection is a blueprint for desaster.

The Backend

There are two ways you can set up the backend part. You can do it with infrastructure as code with frameworks like serverless.com or simply by hand using the Amazon AWS Console. For this article we do it with the console even though its a bit more work but with the advantage that we dont need to install and set up another framework. If you would like to see an aricle explaining that with the serverless framework, feel free to write me.

I assume you already have an Amazon AWS account which you can use.

The first thing after logging in is creating a Lambda function which will act on a URL call from the frontend. This Lambda will do the following:

  • receive the form values from frontend
  • validate the reCaptcha token from frontend against Google backend
  • create an SNS message with data from frontend
  • return a 302 with the URL to show to the user

We will develop the lambda with javascript, but it would be easy enough to port this thing to java or whatever language you feel comfortable with.

'use strict';const AWS = require("aws-sdk");
const completeUrl = "https://www.google.com";
const axios = require('axios');
const reCapUrl = "https://www.google.com/recaptcha/api/siteverify";
// we got this from personal reCaptcha Google Page
const reCaptchaSecret = "yyy" ;
// we need to grab this from Amazon SNS
const snsTopic = "arn:aws:sns:eu-central-1:00000000:MyTopic";
module.exports.webhook = async (event, context, callback) => {
console.log("Starting ContactForm Processing for website form.");
let body = event.body; // process the urlencoded body of the form submit and put it in a
// map structure
let parts = body.split('&');
let result = [];
// grab the params
for (let i = 0, len = parts.length; i < len; i++) {
let kVal = parts[i].split('=');
// replace the + space then decode
let key = decodeURIComponent(kVal[0].replace(/\+/g, ' '));
result[key] = decodeURIComponent(kVal[1].replace(/\+/g, ' '));
}
// its always a good idea to log so that we can inspect the params
// later in Amazon Cloudwatch
console.log(result);
if(result["g-recaptcha-response"] == null) {
console.log("No Captcha supplied. Exit.");
} else {
// verify the result by POSTing to google backend with
// secret and frontend recaptcha token as payload
let verifyResult = await axios.post(reCapUrl, {
secret: reCaptchaSecret,
response: result["g-recaptcha-response"]
});
// if you like you can also print out the result of that. Its
// a bit verbose though
console.log(verifyResult);
if (verifyResult.status === 200) {
// 200 means that Google said the token is ok
// now we create a simple emailbody aka the body of the SNS
// message. If you want to do more than just emailing, you
// should rethink the structure of the message though
let emailbody = "— — Kontaktform (okaycloud.de) — -\n\nName:"+
result["FULLNAME"]+”\nEmail: “+result["EMAIL"]+”\nTel: "+
result["PHONE"]+”\n\nThema: “+result["SUBJECT"]+”\n"+
"Nachricht: "+result["MESSAGE"];
let sns = new AWS.SNS();
let params = {
Message: emailbody,
Subject: “ContactForm: “+result["SUBJECT"],
TopicArn: snsTopic
};

// we publish the created message to Amazon SNS now…
sns.publish(params, context.done);

// now we return a HTTP 302 together with a URL to
// redirect the browser to success URL (we put in
// google.com for simplicty)
callback(null, {
statusCode: 302,
headers: {
Location: completeUrl,
}
});
} else {
console.log("reCaptcha check failed. Most likely SPAM.");
}
}
};

This Code is pretty simple at this point. First we do some form body handling and extract the parameters. After that we call the reCaptcha REST validation service from Google which tells us if the call from the frontend is valid. This means that the caller has not directly requested the backend url and tried to circumvent the spam protection. After the reCaptacha check, we create a SNS message and publish it to our SNS topic. The topic ARN itself will be put in later in the const section because at this point we dont know the ARN yet, since we have not created the topic. After the publusing is done, we just sent a HTTP 302 with a location back to the browser. Dont forget to insert the correct reCaptchaSecret value into the script which you got by setting up the Frontend.

Note: Using decoupling services like Amazon SQS or SNS helps to structure your application in little pieces called microservices. If you want more than email sending, just write another SNS subscriber like a Lambda which adds an entity to your CRM. Its even possible to write that Lambda in another language like Java if a colleague wants to. Goodbye Monolith.

Unfortunately you cant easily put in this script into the AWS Lambda online editor, because it has external dependencies like Axios. So you need to create a little Node project with the following package.json.

{
"name": "myProject",
"version": "0.0.1",
"private": true,
"scripts": {},
"dependencies": {
"aws-sdk": "^2.229.1",
"axios": "^0.18.0",
}
}

After you build the project with NPM or Yarn, you need to ZIP the folder for later upload (including the node_modules folder), which in fact you can do in the Lambda online editor. But dont ZIP it right now because you need the value for the variable snsTopic which you will retrieve in the next section.

Create the SNS Topic

So lets start creating the SNS topic and the Email Subscriber. Simply login to the AWS console and type “SNS” into the services dropdown menu. From there you can simply create the topic.

After creating the topic, we create a “subscription” on that topic. We will use the “Email” Protocol with the advantage that we dont need to code anything. As endpoint just enter the emailaddress of the receipient of the form submit. So basically just use your own email for testing purposes.

As you can see in the screen above, the topic ARN is also mentioned. Its blurred because of privacy reason in this screenshot, but just copy that ARN and paste it into the variable snsTopic and replace the dummy value “arn:aws:sns:eu-central-1:00000000:MyTopic”.

Remember you can add more subscribers. This way you can add additional functionality to your form submit without creating a monolith script or something.

Create the Lambda

Go to the Lambda Service in the AWS console and create a new function by clicking “Create function” button on the upper right. Chose “Author from scratch” and give it a name. As runtime you chose Node.js 8.10.

Create Permission (Role)

For a Lambda function to publish to a SNS topic and write to a log file, it needs some permissions which can be combined in a Role. You can do this manually using the IAM Service in AWS but there is an easier path for that. While still being in the create Lambda screen chose “Create new role from template” in the Role selection list, give it a name and chose “SNS publish policy” for the “Policy templates” field.

After that you press the “Create function” button. The role that gets created will be available as a normal AWS role in IAM. You can reuse it of course for other Lambdas too, as long as they have the same requirements.

Create the API-Gateway

At the end we need to create the API-Gateway and connect it to the previously created Lambda. This way the lambda becomes publicly available via a URL. API Gateway is only one event source for a lambda. Other sources like cron, s3 events, cognito events, SNS subscriber and a lot more are available, so there are plenty triggers available for lambdas which makes them pretty powerful and a perfect candidate for microservices.

As always you can create the API-Gateway from scratch by using the API-GW gui which you can reach via the service selector or you can create it when you click on the created Lambda. We will use the shortcut.

In the designer section of your Lambda, you see on the left hand all the available triggers for a Lambda, where i mentioned some of them before. Here we just click on API Gateway. If you have done that, you see the API Gateway in the tree of the designer as seen in the screen above. When scrolling down, you can configure the needed parts of the API Gateway.

Just select “Create a new API” and define “Open” security, which means everyone can access the URL but since we have a reCaptcha check inside our lambda, the caller definitely needs to come via our web form. Not the best security but its not as open as it seems. The remaining fields like “API name” and “Deployment stage” have usable default. After saving the API (and the Lambda in the upper right) you will see the complete URL in the API Gateway block. This URL needs to be used in the HTML form as “action” value.

Uploading your project

At the end you need to upload your ZIP file to AWS. You can do this also in the Lambda function editor by clicking “Upload a .ZIP file” in the Code entry type select box.

Tired of the AWS console?

As mentioned before, chaining all those AWS service together via AWS Console is a pretty exhausting and its also not reproducible. Using serverless.com the infrastructure code would look like this (serverless.yml):

service: myservice

plugins:
- serverless-iam-roles-per-function

provider:
name:
aws
runtime: nodejs8.10
stage: dev
region: eu-central-1

functions:

contactform:
memorySize:
128
handler: contactform.webhook
description: Webhook for sending email via SMS to info@
timeout: 10 # optional, in seconds, default is 6
iamRoleStatements:
- Effect: "Allow"
Action:
- sns:publish
Resource: "arn:aws:sns:eu-central-1:xxxxx:contactFormTopic"
events:
- http: POST contactform

This code snippet basically sets up all the infrastructure i explained before. You put this into your project folder and execute: serverless deploy.

But this article is not about the serverless framework (https://serverless.com/) but an intro how to implement reCaptcha with an AWS serverless backend. But its always useful to know what is done behind the scenes, even when using a framework like serverless.

As soon as you build production level code, be sure to use infrastructure tools like serverless framework, AWS SAM or others.

If you want to know more about well architected applications on Amazon AWS, feel free to head over to https://okaycloud.de for more infos.

--

--

Marc Logemann
AWS Factory

Entrepreneur & CTO - (AWS) Software Architect, likes Typescript, Java and Flutter, located in the Cloud, Berlin and Osnabrück.