Skip to content
This repository has been archived by the owner on Mar 8, 2018. It is now read-only.

Using reCAPTCHA

Reza Akhavan edited this page Oct 22, 2013 · 1 revision

The goal of this page is to explain the steps you can take to integrate Google's reCAPTCHA service using the npm/re-captcha module.

Google's reCAPTCHA Service:

reCAPTCHA is a free CAPTCHA service that protects your site against spam, malicious registrations and other forms of attacks where computers try to disguise themselves as a human; a CAPTCHA is a Completely Automated Public Turing test to tell Computers and Human Apart. reCAPTCHA comes in the form of a widget that you can easily add to your blog, forum, registration form, etc.

https://developers.google.com/recaptcha/

npm/re-captcha by jacksontian:

recaptcha renders and verifies Recaptcha captchas.

https://npmjs.org/package/re-captcha

Overview

A fellow GitHub friend and Drywall user opened a feature request to optionally include a reCAPTCHA widget for the signup flow. This might be good for some projects to prevent bot spam. Although it's not baked into Drywall, it's pretty simple to add to any form. The following guide will step you through adding a reCAPTCHA widget to the contact form of Drywall.

Step #1 Install the re-captcha module

$ npm install re-captcha --save

Step #2 Credentials & Configuration

You need to get API keys (public and private) for the reCAPTCHA service from Google. If you're just testing this out on your local machine, it's probably best to check the global key option Enable this key on all domains (global key). Obviously in a production environment it makes sense to limit this to your domain.

After you have your API keys add them to your Drywall /config.js file.

exports.recaptcha = {
  key: 'xxxxx-xxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxx',
  secret: 'xxxxx-xxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxx'
};

Then pull these variables into our app in /app.js so we can access them in our view logic.

  //recaptcha settings
  app.set('recaptcha-key', config.recaptcha.key);
  app.set('recaptcha-secret', config.recaptcha.secret);

Step #3 On the Server Side

Inside of /views/contact/index.js we're going to update the init function and include our public reCAPTCHA key as a local variable so we can access it in our markup to be sent to the client.

exports.init = function(req, res){
  res.render('contact/index', {
    recaptchaKey: req.app.get('recaptcha-key')
  });
};

Now we're going to update our sendMessage function by creating an instance of the Recaptcha module and adding a step to our workflow to validate the reCAPTCHA challenge workflow.on('validateRecaptcha'... before validating other form variables workflow.on('validate'....

exports.sendMessage = function(req, res){
  var workflow = req.app.utility.workflow(req, res);
  var Recaptcha = require('re-captcha');
  var recaptcha = new Recaptcha(req.app.get('recaptcha-key'), req.app.get('recaptcha-secret'));
  
  workflow.on('validateRecaptcha', function() {
    var data = {
      remoteip:  req.connection.remoteAddress,
      challenge: req.body.recaptcha_challenge_field,
      response:  req.body.recaptcha_response_field
    };
    
    recaptcha.verify(data, function(err) {
      if (err) {
        workflow.outcome.errfor.recaptcha = 'failed';
        return workflow.emit('response');
      }
      else {
        workflow.emit('validate');
      }
    });
  });
  
  workflow.on('validate', function() {
    if (!req.body.name) {
      workflow.outcome.errfor.name = 'required';
    }
    
    if (!req.body.email) {
      workflow.outcome.errfor.email = 'required';
    }
    
    if (!req.body.message) {
      workflow.outcome.errfor.message = 'required';
    }
    
    if (workflow.hasErrors()) {
      return workflow.emit('response');
    }
    
    workflow.emit('sendEmail');
  });
  
  workflow.on('sendEmail', function() {
    req.app.utility.sendmail(req, res, {
      from: req.app.get('smtp-from-name') +' <'+ req.app.get('smtp-from-address') +'>',
      replyTo: req.body.email,
      to: req.app.get('system-email'),
      subject: req.app.get('project-name') +' contact form',
      textPath: 'contact/email-text',
      htmlPath: 'contact/email-html',
      locals: {
        name: req.body.name,
        email: req.body.email,
        message: req.body.message,
        projectName: req.app.get('project-name')
      },
      success: function(message) {
        workflow.emit('response');
      },
      error: function(err) {
        workflow.outcome.errors.push('Error Sending: '+ err);
        workflow.emit('response');
      }
    });
  });
  
  workflow.emit('validateRecaptcha');
};

Note: we're not using the "rendering" features of npm/re-captcha. This is because we're using client side templating via underscore. We're primarily using it for the "verification" feature.

Now we're going to update our Jade template /views/contact/index.jade to include Google's reCAPTCHA ajax script, pull in our public API key and add other needed markup.

extends ../../layouts/default

block head
  title Contact Us

block neck
  link(rel='stylesheet', href='/views/contact/index.min.css?#{cacheBreaker}')

block feet
  script(src='http://www.google.com/recaptcha/api/js/recaptcha_ajax.js')
  script(src='/views/contact/index.min.js?#{cacheBreaker}')

block body
  div.row
    div.col-sm-6
      div.page-header
        h1 Send A Message
      div
        div#contact
    div.col-sm-6.special
      div.page-header
        h1 Contact Us
      p.lead Freddy can't wait to hear from you.
      i.icon-reply-all.super-awesome
      address 1428 Elm Street &bull; San Francisco, CA 94122
  
  script(type='text/template', id='tmpl-contact')
    form
      div.alerts
        |<% _.each(errors, function(err) { %>
        div.alert.alert-danger.alert-dismissable
          button.close(type='button', data-dismiss='alert') &times;
          |<%= err %>
        |<% }); %>
        |<% if (success) { %>
        div.alert.alert-info.alert-dismissable
          button.close(type='button', data-dismiss='alert') &times;
          | We have received your message. Thank you.
        |<% } %>
      |<% if (!success) { %>
      div.form-group(class!='<%= errfor.recaptcha ? "has-error" : "" %>')
        label.control-label Recaptcha:
        div#recaptcha ...loading...
        span.help-block <%= errfor.recaptcha %>
      div.form-group(class!='<%= errfor.name ? "has-error" : "" %>')
        label.control-label Your Name:
        input.form-control(type='text', name='name', value!='<%= name %>')
        span.help-block <%= errfor.name %>
      div.form-group(class!='<%= errfor.email ? "has-error" : "" %>')
        label.control-label Your Email:
        input.form-control(type='text', name='email', value!='<%= email %>')
        span.help-block <%= errfor.email %>
      div.form-group(class!='<%= errfor.message ? "has-error" : "" %>')
        label.control-label Message:
        textarea.form-control(name='message', rows='5') <%= message %>
        span.help-block <%= errfor.message %>
      div.form-group
        button.btn.btn-primary.btn-contact(type='button') Send Message
      |<% } %>
  script(type='text/template', id='recaptcha-key') #{recaptchaKey}

Step #4 On the Client Side

On the client side /public/views/contact/index.js we add the Recaptcha variable to the jsHint comment at the top so the linter doesn't cry. We also update the render function for the form view to generate the reCAPTCHA widget only if the success variable is false. And finally in the contact method of the view we're going to include the challenge and response variables from the reCAPTCHA widget so they can be validated on the server.

/* global app:true, Recaptcha:false */

(function() {
  'use strict';
  
  app = app || {};
  
  app.Contact = Backbone.Model.extend({
    url: '/contact/',
    defaults: {
      success: false,
      errors: [],
      errfor: {},
      name: '',
      email: '',
      message: ''
    }
  });
  
  app.ContactView = Backbone.View.extend({
    el: '#contact',
    template: _.template( $('#tmpl-contact').html() ),
    events: {
      'submit form': 'preventSubmit',
      'click .btn-contact': 'contact'
    },
    initialize: function() {
      this.model = new app.Contact();
      this.listenTo(this.model, 'sync', this.render);
      this.render();
    },
    render: function() {
      this.$el.html(this.template( this.model.attributes ));
      this.$el.find('[name="name"]').focus();
      
      if (!this.model.get('success')) {
        Recaptcha.create($('#recaptcha-key').text(),
          'recaptcha', {
            theme: "red",
            callback: Recaptcha.focus_response_field
          }
        );
      }
    },
    preventSubmit: function(event) {
      event.preventDefault();
    },
    contact: function() {
      this.$el.find('.btn-contact').attr('disabled', true);
      
      this.model.save({
        name: this.$el.find('[name="name"]').val(),
        email: this.$el.find('[name="email"]').val(),
        message: this.$el.find('[name="message"]').val(),
        recaptcha_challenge_field: Recaptcha.get_challenge(),
        recaptcha_response_field: Recaptcha.get_response()
      });
      
      Recaptcha.destroy();
    }
  });
  
  $(document).ready(function() {
    app.contactView = new app.ContactView();
  });
}());

Use the Force

I hope this was helpful. If you have questions or think this page should be expanded please contribute by opening an issue or updating this page.