Using claudia.js and recaptcha to send emails with AWS Lambda

As part of my ongoing blog posts to explore AWS technologies, this post is just a small update to the last one where I explored, how to send an email using claudia.js.

In addition to the previous one this post will use an S3 bucket for static hosting and use google reCAPTCHA to validate sending of emails.

Prerequisites

You should have the aws CLI installed, as well as node and npm. Your aws setup should allow you to create buckets and lambdas.

Registering on Recaptcha

First, you need to register on recaptcha. You can register via your google account, but you also will have to enter the domain names the captchas should be used for, and we will only find this out, after the S3 bucket has been created. In this example with the defined S3 bucket name and the correct region, the domain would be recaptcha-claudia-form-mailer.s3-website-us-east-1.amazonaws.com. Also you will see the sitekey in the client side integration and the secret in the server side integration part. Both are needed.

Creating a node app

Create a directory and create a package.json like this

{
  "name": "claudia-recaptcha",
  "version": "1.0.0",
  "private": true,
  "description": "An example recaptcha based form mailer",
  "scripts": {
    "lambda-tail": "node_modules/.bin/smoketail -f /aws/lambda/recaptcha-form-mailer",
    "lambda-create": "node_modules/.bin/claudia create --memory 64 --name recaptcha-form-mailer --region us-east-1 --api-module recaptcha-form-mailer --policies policies",
    "lambda-update": "node_modules/.bin/claudia update",
    "lambda-destroy": "node_modules/.bin/claudia destroy"
  },
  "license": "Apache-2.0",
  "devDependencies": {
    "claudia": "1.4.1",
    "smoketail": "0.1.0"
  },
  "dependencies": {
    "aws-sdk": "2.3.19",
    "aws-sdk-promise": "0.0.2",
    "claudia-api-builder": "1.2.1",
    "httpinvoke": "1.4.0",
    "querystring": "0.2.0"
  }
}

Run npm install to install all the modules

Creating the claudia app

var ApiBuilder = require('claudia-api-builder')
var api = new ApiBuilder()
var AWS = require('aws-sdk-promise')
var SES = new AWS.SES()
var httpinvoke = require('httpinvoke');
var querystring = require('querystring');

var sender = 'Spinscale Form Mailer <alr@spinscale.de>'
var recipient = 'Alexander Reelsen <alr@spinscale.de>'
var subject = 'Contact form after captcha'

var PRIVATE_KEY = 'YOUR_PRIVATE_KEY';

api.post('/mail', function (req) {
  if (req.post["g-recaptcha-response"] === null) {
    return { 'status' : 'CAPTCHA_NOT_SET' }
  }

  var options = {
    input: querystring.stringify({response: req.post["g-recaptcha-response"], secret: PRIVATE_KEY, remoteip: req.context.sourceIp }),
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    converters: {
      'text json': JSON.parse,
      'json text': JSON.stringify
    },
    outputType: 'json'
  }
  return httpinvoke('https://www.google.com//recaptcha/api/siteverify', 'POST', options).then(function(res) {
    if (res.body.success) {
      var msg = ''
      delete req.post['g-recaptcha-response']
      for (var key in req.post) {
        msg += key + ': ' + req.post[key] + '\n'
      }

      var email = { Source: sender, Destination: { ToAddresses: [ recipient ] }, Message: { Subject: { Data: subject }, Body: { Text: { Data: msg } } } }
      return SES.sendEmail(email).promise()
        .then(function (data) {
          console.log('Sent mail: ' + msg)
          return { 'status': 'OK' }
        })
        .catch(function (err) {
          console.log('Error sending mail: ' + err)
          return { 'status': 'MAIL_NOT_SENT' }
        })
    } else {
      console.log('Captcha check was not successful: ', res.body)
      return { 'status': 'CAPTCHA_INVALID' }
    }
  }, function(err) {
    console.log('Failure', err);
    return { 'status': 'CAPTCHA_FAILED' }
  });
})

module.exports = api

This app creates a single endpoint called /mail. Even though there are two modules on npm, that claim to support recaptcha v2, none of those I tried actually worked.

That’s the reason that the above endpoint uses the httpinvoke package - a HTTP client without any dependencies in order to stay small. As you can, the client calls the google endpoint and sends a form encoded query string consisting of secret, response and remoteip parameters. Only if the response from google turns out to be successful, then the email is sent to the configured recipient, alr@spinscale.de in this example.

As this is a claudia app, we need to configure the permission to actually be able to send emails, by creating policies/send-email.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail"
            ],
            "Resource": [
                "arn:aws:ses:us-east-1:*:*"
            ]
        }
    ]
}

After this is done, you can deploy the app running npm run lambda-create. The output of that command will contain an URL in the style of "https://YOUR_ID.execute-api.us-east-1.amazonaws.com/, where you can call your endpoint by appending /latest/mail

The execution is ready now. However we need to create a static HTML site, that is calling this endpoint now. In case it is not done yet, the identity of the email recipient needs to be verified.

Verifying mail recipient identity

One last step before writing code is to verify the email address we are sending from.

aws ses verify-email-identity --email-address alr@spinscale.de

Now we can cater for the static content and the corresponding S3 buckets.

Creating an S3 bucket

Create a new bucket and check if it has been created

aws s3api create-bucket --acl public-read --bucket recaptcha-claudia-form-mailer
aws s3api list-buckets

Ensure you create a website for this S3 bucket, which requires us to to configure a (pretty empty) website configuration file. You can also create the directory s3-bucket, which will contain our static assets for the bucket

aws s3 website s3://recaptcha-claudia-form-mailer/ --index-document index.html --error-document error.html
aws s3api get-bucket-website --bucket recaptcha-claudia-form-mailer

Normally, you should be able to reach the website now under http://recaptcha-claudia-form-mailer.s3-website-us-east-1.amazonaws.com/ - in case that I deleted my S3 bucket or no one else has created a bucket by this name in between.

TODO: I have not found a way to receive the concrete URL of the bucket via aws CLI tool, maybe I just missed it.

Creating a static site for S3

Only two HTML files are needed, index.html for sending the mail, and error.html for any 4xx errors, which is returned automatically by S3.

Let’s start with the simple error.html file:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.1.0/milligram.min.css">
</head>
<body>

<main class="wrapper">
  <section class="container">
    <h1>Error occured</h1>
    <p>
    Something fishy occured. Try going back to <a href="/">main page</a>
    </p>
  </section>
</main>

</body>
</html>

The only specialty here is the use of the milligram CSS framework, which covers a bit for my bad design capabilities.

Now the index.html file, which we will go through step-by-step in a second

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
  <script src='https://www.google.com/recaptcha/api.js'></script>

  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.1.0/milligram.min.css">
<script type="text/javascript">
var endpoint = "https://YOUR_ENDPOINT.execute-api.us-east-1.amazonaws.com/latest";

function showButton() {
  $('.button-primary').show()
}

$(document).ready(function() {
  $('#form').submit(function(event) {
    event.preventDefault()
    $.post(endpoint + '/mail', $("#form").serialize(), function (resp) {
      grecaptcha.reset()
      $('button-primary').hide()
    }, 'json')
  })
})
</script>
</head>
<body>

<main class="wrapper">
  <div class="container">
    <h1>Recaptcha Example</h1>
    
    <div class="row">
    <form id="form">
      <fieldset>
        <div class="g-recaptcha" data-sitekey="YOUR_PUBLIC_KEY" data-callback="showButton"></div>
        <div style="padding-bottom:0.5em" id="recaptcha"></div>
        <label for="nameField">Name</label>
        <input type="text" placeholder="Your awesome name" id="nameField" name="name">
        <label for="commentField">Comment</label>
        <textarea placeholder="Hello, trying the captcha" id="commentField" name="comment"></textarea>
        <input class="button-primary" type="submit" value="Send" style="display: none">
      </fieldset>
    </form>
    </div>
  
  </div>
</main>

</body>
</html>

As you can see, there is no big things hidden in here. The recaptcha JS files need to be loaded, the endpoint of the lambda needs to be defined. The only fancy thing here is the fact, that the send button only occurs, once the captcha is answered.

In addition, upon form submit, the submit event is caught and an AJAX call is sent instead. Obviously in a real application you should display a message or something.

Now, as everything is ready, the HTML files need to be uploaded.

Uploading the HTML files

Uploading is dead simple using aws. The first command is just a dry run to show what would be uploaded, where as the exclude is just to exclude files starting with a dot, in case there is still a vim .swp file, because you have opened one of the files.

aws s3 sync --dryrun --acl public-read s3-bucket/ s3://recaptcha-claudia-form-mailer/ --exclude ".*"
aws s3 sync --acl public-read s3-bucket/ s3://recaptcha-claudia-form-mailer/ --exclude ".*"

Now opening the URL of your S3 bucket should show you a website with a captcha and once this is filled out correctly, you should find a send button and get an email after filling it out. In order to debug the calls of your lambda, you can run npm run lambda-tail in the console.

Also you can try and open a non-existing URL in your bucket and you should get back the error HTML page.

The final overview

This is how your file structure should look like after creating all needed files.

# tree -I node_modules
.
├── claudia.json
├── package.json
├── policies
│   └── send-email.json
├── recaptcha-form-mailer.js
└── s3-bucket
    ├── error.html
    └── index.html
2 directories, 6 files

Next

As you can see here, getting a static site up and running is pretty simple, using recaptcha with AWS Lambda as well. So the next steps are the following (I plan to make a series out of this sooner or later)

  • Starting to write real apps (+ testing) using lambda. Next step is getting OAuth using Cognito up and running
  • Setting up a full static website using S3, Route 53, CloudFront and self updating SSL certs only via the aws CLI tool. Once that works and can be automated I will move over this website to S3. I recently moved the devcampmuc over there (configured everything via the browser UI), but this domain is a bit more complex.
  • Better deployment. When deploying a lambda and a S3 bucket, this should both be uploaded with a single command (which is a simple npm run script)