Adding reCAPTCHA to your Single Page Applications

At some point, you were probably creating a “Contact Us” page for a website, and chances are, you also used reCAPTCHA to prevent spam emails from being sent to your mailbox from the “contact” page.

In this post, I am sharing how I added reCAPTCHA to a Single Page Application, using the HotTowel SPA Visual Studio Template.

The View

To make this article simple, we will add a contact form to the home view that comes with the HotTowel SPA template.

<section>
    <h2 class="page-title" data-bind="text: title"></h2>
    <div class="row-fluid">
        <div class="span12">
            <form class="form-horizontal">
                <div class="control-group">
                    <label class="control-label" for="inputName">Your Name</label>
                    <div class="controls">
                        <input type="text" id="inputName" placeholder="Your Name Please" data-bind="value: senderName" />
                    </div>
                </div>
                <div class="control-group">
                    <label class="control-label" for="inputEmail">Your Email</label>
                    <div class="controls">
                        <input type="email" id="inputEmail" placeholder="Email Address" data-bind="value: senderEmail" />
                    </div>
                </div>
                <div class="control-group">
                    <label class="control-label" for="message">Your Message</label>
                    <div class="controls">
                        <textarea id="message" placeholder="Your Message" rows="3" data-bind="value: senderMessage"></textarea>
                    </div>
                </div>
                <div class="control-group">
                    <div class="controls">
                        <div id="captchadiv"></div>
                    </div>
                </div>
                <div class="control-group">
                    <div class="controls">
                        <button type="submit" class="btn btn-info" data-bind="click: sendMessage">Send</button>
                    </div>
                </div>
            </form>
        </div>
    </div>
</section>

Take a closer look at the view, and you will notice that I have a DIV element there with an ID of “captchadiv”. That will come into play as we go along.

View Model Stuff

The view model is just a typical knockoutJs view model code. Recaptcha is being required here by shimming it during the configuration part of requireJs initialization at main.js code.

require.config({
    urlArgs: "ts=" + new Date().getTime(), // disable caching - remove in production
    waitSeconds: 15,
    paths: {
        "text": "durandal/amd/text",
        "recaptcha": "//www.google.com/recaptcha/api/js/recaptcha_ajax"
    },
    shim: {
        "recaptcha": { exports: 'Recaptcha' }
    }
});

The view model

define(['services/logger',
        'services/datacontext',
        'recaptcha'],
       function (logger, datacontext, Recaptcha) {
           "use strict";
           var title = 'Connect With Me',
               senderName = ko.observable(""),
               senderEmail = ko.observable(""),
               senderMessage = ko.observable(""),
               viewAttached = function () {
                   Recaptcha.create("YOUR_PUBLIC_KEY_HERE", 'captchadiv', {
                       tabindex: 4,
                       theme: "clean"
                   });
               },
                sendMessage = function () {

                    var data = {
                        Name: senderName(),
                        Email: senderEmail(),
                        Message: senderMessage(),
                        Challenge: Recaptcha.get_challenge(),
                        Response: Recaptcha.get_response()
                    };

                    datacontext.sendContactMessage(data)
                    .always(function (res, textStatus, jqXHR) {
                        if (res.status == 200) {
                            senderName("");
                            senderEmail("");
                            senderMessage("");
                            logger.log('I will get back to you as soon as I can', null, 'home', true);
                        } else {
                            logger.logError(res.responseText, null, 'home', true);
                        }
                        Recaptcha.reload();
                    });

                },
               activate = function () {
                   logger.log('Home View Activated', null, 'home', true);
                   return true;
               };

           return {
               activate: activate,
               title: title,
               viewAttached: viewAttached,
               senderName: senderName,
               senderEmail: senderEmail,
               senderMessage: senderMessage,
               sendMessage: sendMessage
           }

       });

Data Context

define(['services/dataservice'], function (dataservice) {
    "use strict";
    var sendContactMessage = function (param) {
        return dataservice.sendContactMessage(param);
    };

    return {
        sendContactMessage: sendContactMessage
    }
});

Data Service

define(['durandal/http'], function (http) {
    "use strict";
    var sendContactMessage = function (data) {
        return http.post('breeze/Contact/Send', data);
    };

    return {
        sendContactMessage: sendContactMessage
    }
});

Web API Controller

Basically, what the “back-end” needs to do, is to post the user’s response to reCAPTCHA’s server, to check if it is correct. Please checkout the documentation for more details on their protocol.

using Recaptcha.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;

namespace Recaptcha.Controllers
{
    public class ContactController : ApiController
    {
        [HttpPost]
        public async Task&lt;HttpResponseMessage&gt; Send(ContactModel data)
        {
            var httpClient = new HttpClient();
            var PK = "YOUR_PRIVATE_KEY_HERE";
            var userIP = ((HttpContextBase)this.Request.Properties["MS_HttpContext"]).Request.UserHostAddress;
            var uri = "http://www.google.com/recaptcha/api/verify";

            var postData = new List&lt;KeyValuePair&lt;string, string&gt;&gt;();
            postData.Add(new KeyValuePair&lt;string, string&gt;("privatekey", PK));
            postData.Add(new KeyValuePair&lt;string, string&gt;("remoteip", userIP));
            postData.Add(new KeyValuePair&lt;string, string&gt;("challenge", data.Challenge));
            postData.Add(new KeyValuePair&lt;string, string&gt;("response", data.Response));

            HttpContent content = new FormUrlEncodedContent(postData);

            string responseFromServer = await httpClient.PostAsync(uri, content)
                    .ContinueWith((postTask) =&gt; postTask.Result.EnsureSuccessStatusCode())
                    .ContinueWith((readTask) =&gt; readTask.Result.Content.ReadAsStringAsync().Result);

            if (responseFromServer.StartsWith("true"))
            {
                // TODO: send an email blah blah

                return new HttpResponseMessage(HttpStatusCode.OK);
            }
            else
            {
                return Request.CreateResponse(HttpStatusCode.ExpectationFailed, "Sorry mate, wrong captcha response. Are you a bot?");
            }

        }
    }
}
Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.
Tagged with: , , , ,
Posted in SPA
  • Martin Klemsa

    This is a great example. Thank you!

  • Martin Klemsa

    Actually I have a question. Your code for loading recaptcha through require.js config works locally, but when building a release main-built.js I can’t get the script to load and I get an error when loading the viewmodel that recaptcha is missing.
    Have you come across this one? Thanks a lot again.

    • golfer75

      Hey Martin,

      Did you ever get this problem resolved? If not maybe I can help you get it straightened out.

      • Martin Klemsa

        Unfortunately not, my crude solution was to put

        directly into index.cshtml after my bundles… Didn’t want to waste time as I’m no expert on Require or making bundles work with CDN.

        • golfer75

          Hmm, ok. In your require.config section in your app, you might have a paths object where you specify the paths of modules within your application. Here you may be loading in jQuery, backbone, handlebars, etc. Is that the case? If so, you should be able to simply add the recaptcha file location there, like:

          ‘recaptcha': ‘http://www.google.com/recaptcha/api/js/recaptcha_ajax’,

          (Remember also to omit the .js at the end of that path)

          Then, in your view (assuming you’re using a backbone view, for example), you can load that file like so:

          require(‘recaptcha’);

          Does that make sense?

          If you’d like more help, send me your email address and I’d be glad to help you more offline. I just got this working last night within my backbone app, so I know it’s possible.

          • Martin Klemsa

            That is indeed what I did, just like this article says. And it worked fine in debug. My problem was with release version built with Grunt which uses r.js instead of require.js (or so I believe).

          • golfer75

            Ohh — gotcha. I’ll try to build my app and see if I run into the same issue.

          • golfer75

            Hey Martin,

            I got it all figured out, and the solution is pretty simple. In my view (which is a Backbone view), I just put this line toward the top:

            require(‘http://www.google.com/recaptcha/api/js/recaptcha_ajax.js’);

            From there I built my app to get an optimized file. Then, I loaded the app and voila, it works. I checked the network tab to confirm: first, the minimized/optimized file is loaded, then a network call or two later, recaptcha_ajax.js is loaded from Google.

            Hope that helps.

          • Martin Klemsa

            Hiya, cheers. I tried that but again only works in debug. What build system do you use? (I use Grunt). When trying to load the built file I get “undefined missing http://www.google.com/recaptcha/api/js/recaptcha_ajax.js
            This is getting ridiculous!

          • golfer75

            Oh, hmm. I’m using require’s build command/script/whatever, which is r.js. I don’t have anything really special about my setup.

            Would you be willing to send me your email address so we can communicate offline? I’d be glad to help get your project straightened out, I just feel like I’m flying blindly by not seeing how you have stuff setup, etc. Lemme know.

          • Martin Klemsa

            Sure. m.klemsa at gmail.com.
            Can you send me the first few lines of your JS file where you include the recaptcha script?

  • ericpanorel

    I think this has something to do, with the “bundling” process. You probably have to find a way, to have it included

  • Shalini Lakshmanan

    can someone please explain the use of “{export:”Recaptcha”}” within shim ?