DEV Community

Sophie DeBenedetto
Sophie DeBenedetto

Posted on

DIY reCAPTCHA with Ruby

This post was originally published on my personal blog, The Great Code Adventure.

reCAPTCHA is an anti-bot tool you can implement in your web app to prevent bad actors from programmatically filling out forms and spamming your endpoints. In this post, we'll implement Google's reCAPTCHA protocol using the Google reCAPTCHA 2 client-side library and our very own hand-rolled verification client.

I am Not a Robot

CAPTCHA is a mechanism through which we can programmatically determine whether a user is a human or a bot. CAPTCHA actually stands for "Completely Automated Public Turing test to tell Computers and Humans Apart". A typical CAPTCHA will ask the user to submit some input that only a human (or a Cylon? That part is still a little fuzzy...) can provide. For example, typing in some blurry letters from an image, selected a set of images that contain a car or a street sign.

source

Captcha has many applications--preventing spam comments, stopping bots from registering for your app, and fighting DDoS attacks in the form of bot-driven logins, to name a few.

Google reCAPTCHA 2

Google offers a relatively new CAPTCHA protocol--"reCAPTCHA"––which allows users to verify that they are not robots with the simple click of a button. No need to "solve" a traditional CAPTCHA by typing in words or identifying pictures.

Integrating Google reCAPTCHA is pretty straightforward. First thing's first though, we need to register our app with the Google reCAPTCHA service and receive our application's site key and secret key.

  1. Log in to your Google account and visit https://www.google.com/recaptcha/intro/, click the "Get reCAPTCHA" button on the right hand side.
  2. On the next page, select "reCAPTCHA 2" from the "type of reCAPTCHA" list and enter your application's domain.
  3. Lastly, grab your site key and secret key from the "keys" section of the next page. Note the client-side and server-side instructions. We'll use these guides to implement reCAPTCHA on our site.

Implementing reCAPTCHA on the Client-Side

We'll be using reCAPTCHA for the sign up page of our Rails application. We want our users to verify that they are not bots before submitting the sign up form.

First, we need to load the reCAPTCHA library. We'll do so in the application layout, so that it's available on any page we want to use it.

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    ...
    <script src='https://www.google.com/recaptcha/api.js'>
    </script>
  </head>

  <body>
    <%= render partial: 'layouts/navbar' %>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

There are two ways to include reCAPTCHA on a particular page––automatically and explicity.

To automatically render the reCAPTCHA widget on the page, we simply add the following anywhere on the page:

<div class="g-recaptcha" data-sitekey="your_site_key"></div>

However, we want a little more control over the appearance and behavior of our widget. For example, we want to set the theme and language of the widget. For this level of control, we'll use the explicit render approach.

To explicitly render the reCAPTCHA widget, we'll do the following:

  1. Define an onload callback function, and insert the reCAPTCHA resource.
  2. Leverage the reCAPTCHA grecaptcha.render function to configure reCAPTCHA.
  3. Define the HTML element on the load of which the reCAPTCHA will render.

Let's define our onload function and insert the Javascript resource. Note that when we insert the resource, we are setting the onload parameter to the name of our onload callback function, onRecaptchaElementLoad, and the render parameter to explicit. Also note that we are using the async defer flag for our script tag, to ensure that the script is not run before the rest of the page loads.

# app/views/registrations/new.html.erb
<%=javascript_tag do %>
  var onRecaptchaElementLoad = function() {
    // coming soon!
  };
<%end%>

<script src='https://www.google.com/recaptcha/api.js?onload=onRecaptchaElementLoad&render=explicit' async defer></script>

Now let's define our function body to use the grecaptcha.render function. This function takes in two arguemtns:

  1. The ID of the element whose load will trigger this callback.
  2. A set of options for configuring the reCAPTCHA widget.
# app/views/registrations/new.html.erb
<%=javascript_tag do %>
  var onRecaptchaElementLoad = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : '<%= j ENV["REACAPTCHA_SITE_KEY"]%>',
      'hl': '<%= j locale %>',
      'theme': 'dark'
    });
  };
<%end%>
...

The sitekey param is required, and is set to the value of the site key we received when we registered our app with the Google reCAPTCHA service. I added my site key to my ENV vars with the help of the dotenv gem.

We also set optional params hl, the language designation, and theme. This snippet assumes we have a view helper method locale which returns a valid language code ("en", "de", "fr", etc).

Now that we've defined our callback function, let's add our reCAPTCHA element, a div with an id of "reCAPTCHA", to the page. This element should be added within the registration form.

# app/views/registrations/new.html.erb
<%= form_for @user do |f| %>
  ...
  <div id="recaptcha"></div>
  <%= f.submit %>
<% end %>

That's it for our client-side integration. Our form will now submit with the following param:

{"g-recaptcha-response" => "<recaptcha code>"}

Let's move on to our server-side implementation.

Server-Side

We'll verify our reCAPTCHA response as a before_action in our controller. Although we are only placing the reCAPTCHA widget on our registration form right now, I absolutely anticipate using reCAPTCHA on other pages. For example, let's say a few weeks from now I find my sign in or password reset pages being spammed by a lot of bot form submission attempts. I want the ability to quickly and easily include reCAPTCHA verification to these and potentially other controllers in the future. And I want to do so without repeating code.

So, in anticipation of this future requirement, and with the desire to keep our code DRY always in mind, we'll put our reCAPTCHA verification code in a controller module that we'll include in the RegistrationsController.

Verifying reCAPTCHA in the Controller Module

We'll define our module in app/controllers/concerns. It will call a before action which will in turn call on our very own reCAPTCHA verification service (coming soon!). It will redirect if the verification failed. It will leverage ActiveSupport::Concern to get access to some nice clean included; do syntax.

# app/controllers/concerns/recaptcha_verifiable.rb
require 'active_support/concern'

module RecaptchaVerifiable
  extend ActiveSupport::Concern

  included do
    before_filter :recaptcha, only: [:create]
  end

  def recaptcha
    reroute_failed_recaptcha && return unless RecaptchaVerifier.verify(params["g-recaptcha-response"], request.ip)
  end

  def reroute_failed_recaptcha
    @person           = Person.new
    flash.now[:error] = "Please verify you are not a robot."
    render action: "new"
  end
end

We'll include this module in our RegistrationsController, and we can include it in any controller to which we want to add recaptcha in the future.

# app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
  include RecaptchaVerifiable
  ...
end

Now that we've taught our controller how to verify reCAPTCHA, and what to do if that verification fails, let's build out of verification service, RecaptchaVerifier.

Building our own reCAPTCHA Verifier

We'll define our verifier in app/services. Our service will have one responsibility: given a reCAPTCHA response, return true if the response is valid, and return false if not.

Let's think about this responsibility for a moment. In order for our verifier service to carry this responsibility, does it need to know about the reCAPTCHA mechanism, i.e. that we are using Google reCAPTCHA? No!

Our service will use dependency injection to call on a recaptcha_client. That client will be responsible to enacting the reCAPTCHA verification via an external API, in our case the Google reCAPTCHA API.

# app/services/recaptcha_verifier.rb
class RecaptchaVerifier
  def self.verify(response, remote_ip, recaptcha_client=GoogleRecaptcha)
    new(response, remote_ip, recaptcha_client).verify
  end

  def initialize(response, remote_ip, recaptcha_client)
    @recaptcha_response = response
    @remote_ip          = remote_ip
    @recaptcha_client   = recaptcha_client.new
  end

  def verify
    return false unless recaptcha_response
    recaptcha_client.verify_recaptcha(response: recaptcha_response, remoteip: remote_ip)
  rescue
    false
  end

  private

  attr_reader :recaptcha_client, :recaptcha_response, :remote_ip
end

Here, we're passing in the reCAPTCHA response from the params, the IP address (this is a param that the Google reCAPTCHA API requires, and we'll take a look at that API soon), and a third, optional argument of the reCAPTCHA client. We default that client to the GoogleRecaptcha, a class that we will define next to handle the actual Google reCAPTCHA API request.

Our service is pretty simple. It:

  • Returns false in the absence of a reCAPTCHA response (this could happen if our controller endpoint gets hit without the "g-recaptcha" param––a sure sign of a bad actor)
  • Calls on the client to verify the reCAPTCHA response, returning the value of that call, which we assume will be true or false.

Now we're ready to build our GoogleRecaptcha client.

Building our own Google reCAPTCHA Client

Our Google reCAPTCHA client will act as the actual verification mechanism in our reCAPTCHA flow. As such, it has one job: send the verification request to the Google reCAPTCHA API and parse the response.

We'll define our client in lib/.

# lib/google_recaptcha.rb
class GoogleRecaptcha
  BASE_URL   = "https://www.google.com/".freeze
  VERIFY_URL = "recaptcha/api/siteverify".freeze

  def initialize
    @client = Faraday.new(BASE_URL)
  end

  def verify_recaptcha(params)
    response = perform_verify_request(params)
    success?(response)
  end

  def success?(response)
    JSON.parse(response.body)["success"]
  end

  private

  attr_reader :client

  def perform_verify_request(params)
    client.post(VERIFY_URL) do |req|
      req.params = params.merge({secret: ENV["RECAPTCHA_SECRET_KEY"]})
    end
  end
end

Our client expects to receive an argument of a hash containing the Google reCAPTCHA API's required params of remoteip (pointing to the IP of the original user's request) and response (pointing to the response that came through from the "g-recaptcha" param of the original request).

To these params, we add the key/value pair: {secret: ENV["RECAPTCHA_SECRET_KEY"]} (assuming we've set our secret key to an ENV var).

This allows us to send the verification request to the Google API's verification endpoint: https://www.google.com/recaptcha/api/siteverify

Google will respond to this request with the following:

{
  "success": true|false,
  "challenge_ts": timestamp, 
  "hostname": string,
  "error-codes": [...] 
}

We will parse the JSON of the response body to return true or false depending on the value of the "success" key.

And that's it! That allows us to call on our service in our controller before_action:

# app/controllers/registrations_controller.rb, by way of the RecaptchaVerifiable module

RecaptchaVerifier.verify(recaptcha_response, ip_address)

Which will in turn call on our GoogleRecaptcha client to return true or false, depending on the validity of the reCAPTCHA response.

Your site is now officially robot proof!


source

Top comments (0)