ActAsTextcaptcha

<img src=“https://secure.travis-ci.org/matthutchinson/acts_as_textcaptcha.png” alt=“Travis Build Status” align=“absmiddle”/> <img src=“https://codeclimate.com/badge.png” align=“absmiddle” alt=“Code Climate Stats” />

ActsAsTextcaptcha provides spam protection for your Rails models using logic questions from the excellent Text CAPTCHA web service (by Rob Tuley of Openknot). It is also possible to configure your own questions and answers instead of using this API (or as a fall back in the unlikely event of the web service being unavailable)

The gem makes use of bcrypt encryption when comparing a guess with the correct answer(s). This gem is actively maintained, has good test coverage and is compatible with Rails 2.3.*, Rails 3 and 4. If you have any issues please report them here.

Text CAPTCHA’s logic questions are aimed at a child’s age of 7, so they can be solved easily by even the most cognitively impaired users. As they involve human logic, questions cannot be solved by a robot. There are both advantages and disadvantages for using logic questions over image based captchas, find out more at Text CAPTCHA.

Demo

Here’s a fully working demo on heroku!

Installing

Rails

Just add the following to your Gemfile, then ‘bundle install`;

gem 'acts_as_textcaptcha'

Rails 2.3.*

Add this to your environment.rb file, then ‘gem install acts_as_textcaptcha`;

config.gem 'acts_as_textcaptcha'

Or as a plugin

If you do decide to install this as a Rails plugin, you’ll have to manually include the bcrypt-ruby gem in your Gemfile or environment config. Like so;

gem 'bcrypt-ruby', :require => 'bcrypt'
OR
config.gem 'bcrypt-ruby', :lib => 'bcrypt'

Then install the plugin with rails or script/rails as follows;

rails plugin install git@github.com:matthutchinson/acts_as_textcaptcha.git
OR
script/plugin install git@github.com:matthutchinson/acts_as_textcaptcha.git

Setting up

NOTE: The procedure for configuring your app *changed significantly* from v2.* to v3.* If you are having problems please carefully check the instructions below.

First grab an API key for your website then open you Rails console (or any irb session) and generate a new BCrypt salt by typing;

require 'bcrypt';BCrypt::Engine.generate_salt

Next add the following code to models you’d like to spam protect;

class Comment < ActiveRecord::Base
  # (this is the most basic way to configure the gem, with an API key and Salt only)
  acts_as_textcaptcha :api_key     => 'PASTE_YOUR_TEXTCAPTCHA_API_KEY_HERE',
                      :bcrypt_salt => 'PASTE_YOUR_BCRYPT_SALT_HERE'
end

Next in your controller’s new action you’ll want to generate the logic question for your model, like so;

def new
  @comment = Comment.new
  @comment.textcaptcha
end

Finally, in your form view add the spam question and answer fields using the textcaptcha_fields helper. You are free to arrange the HTML within this helper as you like;

<%= textcaptcha_fields(f) do %>
  <div class="field">
    <%= f.label :spam_answer, @comment.spam_question %><br/>
    <%= f.text_field :spam_answer, :value => '' %>
  </div>
<% end %>

NOTE: For Rails 2.* this step is slightly different (<%= changes to <%);

<% textcaptcha_fields(f) do %>
  <div class="field">
    <%= f.label :spam_answer, @comment.spam_question %><br/>
    <%= f.text_field :spam_answer, :value => '' %>
  </div>
<% end %>

NOTE: If you’d rather NOT use this helper and prefer to write your own view code, see the html produced from the textcaptcha_fields method in the source code.

Toggling Textcaptcha

You can toggle textcaptcha on/off for your models by overriding the ‘perform_textcaptcha?` method. If you overridde it to return false, no questions will be fetched from the web service and textcaptcha validation is not performed. Additionally the `textcaptcha_fields` form helper will render nothing. This is useful for writing your own logic to disable spam protection for logged in users etc.

For flexibility you can also use a ‘skip_textcaptcha` attribute (protected from mass-assignment) to skip the textcaptcha validation step only. This is helpful when you need to bypass spam protection after a question has been generated and `perform_textcaptcha?` is true.

More Configurations

The gem can be configured for models individually (as shown above) or with a config/textcaptcha.yml file for the whole app. A config must have a valid bcrypt_salt, and an api_key and/or an array of questions defined.

Hash

class Comment < ActiveRecord::Base
  acts_as_textcaptcha :api_key     => 'YOUR_TEXTCAPTCHA_API_KEY',
                      :bcrypt_salt => 'YOUR_BCRYPT_SALT',
                      :bcrypt_cost => '3',
                      :questions   => [{ 'question' => '1+1', 'answers' => '2,two' },
                                       { 'question' => 'The green hat is what color?', 'answers' => 'green' }]
end
  • api_key (get from textcaptcha.com/api)

  • bcrypt_salt - used to encrypt the valid possible answers

  • bcrypt_cost - an optional logarithmic number which determines how computationally expensive the bcrypt hash will be (a cost of 4 is twice as much work as a cost of 3 - the default is 10)

  • questions - an array of question and answer hashes (see above) A random question from this array will be asked if the web service fails OR if no API key has been set. Multiple answers to the same question are comma separated (e.g. 2,two) so don’t use commas in your answers!

YAML config

The gem comes with a handy rake task to copy over a textcaptcha.yml template to your Rails config directory. It will also generate a random BCrypt Salt when you first run it.

rake textcaptcha:config

NOTE: If you are on Rails 2.3.*, you’ll have to add the following to your Rakefile to make this task available;

# load textcaptcha rake tasks
Dir["#{Gem.searcher.find('acts_as_textcaptcha').full_gem_path}/lib/tasks/**/*.rake"].each { |ext| load ext } if Gem.searcher.find('acts_as_textcaptcha')

Confguring without the Text CAPTCHA web service

To use only your own logic questions simply ommit the api_key from your config and define at least 1 logic question/answer.

Translations

Recent versions of the gem use standard Rails I18n translation for the error message generated by acts_as_textcaptcha, with a fall-back to English. Unfortunately in some versions of Rails 2.3.* this translation fall-back fails. For this case (and when translating) setup your own locale file with this hierarchy (assuming your model is Comment);

en:
  activerecord:
    errors:
      models:
        comment:
          attributes:
            spam_answer:
              incorrect_answer: "is incorrect, try another question instead"
  activemodel:
    attributes:
      comment:
        spam_answer: "Spam answer"

NOTE: currently the Text CAPTCHA API web service only offers logic questions in English.

Without Rails or ActiveRecord

Although this gem has been built with Rails in mind, is entirely possible to use it without ActiveRecord, or Rails. For an example, take a look at the Contact model used in the test suite here.

Testing and docs

In development you can run the tests and rdoc tasks like so;

  • rake test (all tests)

  • rake test:coverage (all tests with code coverage reporting)

  • rake rdoc (generate docs)

What does the code do?

The gem contains two parts, a module for your ActiveRecord models, and a single view helper method.

A call to @model.textcaptcha in your controller will query the Text CAPTCHA web service. A GET request is made with Net::HTTP and parsed using the default Rails ActiveSupport::XMLMini backend (or the standard XML::Parser in older versions of Rails). A spam_question and an encrypted array of possible answers are assigned to the model.

validate_textcaptcha is called on @model.validate and checks that the @model.spam_answer matches one of the possible answers (decrypted). This validation is only carried out on new records, i.e. never on edit, only on create. All attempted spam answers are case-insensitive and have trailing/leading white-space removed.

If an error or timeout occurs in loading or parsing the API, ActsAsTextcaptcha will fall back to choose a random logic question defined in your options. If the web service fails or no API key is specified AND no alternate questions are configured, the @model will not require spam checking and will pass as valid.

For more details on the code please check the documentation. Tests are written with MiniTest and code coverage is provided by SimpleCov

Requirements

What do you need?

  • Rails >= 2.3.2 (including Rails 3 and 4)

  • Ruby >= 1.8.7 (also tested with REE, RBX, 1.9.2, 1.9.3, 2.0.0)

  • bcrypt-ruby gem (to securely encrypt spam answers)

  • Text CAPTCHA API key (optional, since you can define your own logic questions)

  • MiniTest (optional if you want to run the tests, built into Ruby 1.9)

  • SimpleCov (optional if you want to run the tests with code coverage reporting)

Who’s who?

Gem Signing

You can use the HighSecurity policy to install this gem by adding my public certificate as a trusted source. To do so, run;

gem cert --add <(curl -Ls https://gist.github.com/matthutchinson/5697493/raw/13c2c9edf52cbf72ef399cd4e3b9044ac700abf9/gem-public_cert.pem)

After doing this, you should be able to install the gem with;

gem install acts_as_textcaptcha -P HighSecurity
# or with Bundler, specify the HighSecurity trust policy
bundle install --trust-policy HighSecurity

Todo

  • Achieve 100% test coverage, currently at 93%

  • Remove support for Rails 2.3.* in a future gem release (clean code and tests for this)

Usage

This gem is used in a number of production websites and apps. It was originally extracted from code developed for Bugle. If you’re happily using acts_as_textcaptcha in production, let me know and I’ll add you to this list!