ReCaptcha for Rails - With AJAX Validation

Purpose

This plugin implements view helpers to generate ReCaptcha code, to interface with the HTTP API for captcha verification, a DSL to generate a before_filter and the necessary hacky code to implement AJAX captcha validation.

Implementation

For AJAX validation, a request to ReCaptcha HTTP server must be made in order to verify the user input, but captchas can be checked only once.

The plugin, thus, in case of a successful verification, saves uses the Rails flash to temporarily save this status in the session and then the before filter skips the verification via the ReCaptcha HTTP service.

Installation

Via RubyGems:

gem install panmind-recaptcha

Or via Rails Plugin:

rails plugin install git://github.com/Panmind/recaptcha.git

Usage

In your config/environment.rb:

Panmind::Recaptcha.set(
  :private_key => 'your private key',
  :public_key  => 'your public key
)

In your controller, say, the UsersController for a signup action:

require_valid_captcha :only => :create

private
  def invalid_captcha
    @user = User.new params[:user]
    @user.errors.add_to_base('Captcha failed')

    render :new, :layout => 'login'
  end

The invalid_captcha method is called by the plugin when captcha verification fails, and must be overwritten or a NotImplementedError exception will be thrown.

In your view:

<%= recaptcha :label => 'Are you human?', :theme => 'clean' %>

You can pass any RecaptchaOptions valid option, as stated by the service documentation. The only nonstandard option :label is used by the plugin to print a label before the captcha widget.

AJAX Validation

To cache the results of a successful captcha verification, you need simply to pass the :ajax => true option to the require_valid_captcha controller method.

require_valid_captcha :only => :create, :ajax => true

When the form is validated via AJAX, the maybe successful result will be saved in the flash (thus set in the session store); when the form is then submitted via a plain HTTP request, verification will be skipped.

On Panmind we use our jquery.ajax-validation plugin, that you can download from http://github.com/Panmind/jquery-ajax-nav and the Javascript code located in the js/signup-sample.js file.

On the backend, an example checker that returns different HTTP status code follows:

def 
  # If no email was provided, return 400
  if params[:email].blank?
    render :nothing => true, :status => :bad_request and return
  end

  email = CGI.unescape(params[:email])

  # more thorough checks on email should go here

  # If an user with this email already exist, return 406
  if User.exists?(['email = ?', email])
    render :nothing => true, :status => :not_acceptable and return
  end

  unless valid_captcha?
    invalid_captcha and return
  end

  save_solved_captcha # This method sets a flag in the flash
  render :nothing => true, :status => :ok
end

Moreover, the client Javascript code should be informed via an HTTP status when validation fails, thus the invalid_captcha must contain a special render when the request comes from XHR:

def invalid_captcha
  # If the captcha is not valid, return a 412 (precondition failed)
  render :nothing => true, :status => 412 and return true if request.xhr?

  # Same invalid_captcha code as above
end

The latest XHR specification from the w3c states that cookies set by responses to requests sent via XHR are to be honored by the browser.

The code was tested with IE (6,7,8), Safari (4, 5), Firefox 3, Chrome 5 and Opera 10.

Security

As long as you use a session store backed on the server or cryptographically sign the cookies used by the session cookie store (as Rails does by default) there is no way to bypass the captcha when AJAX validation is enabled.

Compatibility

Tested with Rails 3.0.3 running under Ruby 1.9.2p0. running under Ruby 1.9.1-p378.