DIY Google reCAPTCHA with Ruby
DIY Google reCAPTCHA with Ruby
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.
Captcha has many applications–preventing spam comments, stopping bots from registering for your app, fighting DDoS attacks in the form of bot-driven logins.
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.
- Log in to your Google account and visit https://www.google.com/recaptcha/intro/, click the “Get reCAPTCHA” button on the right hand side.
- On the next page, select “reCAPTCHA 2” from the “type of reCAPTCHA” list and enter your application’s domain.
- 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 useing 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 in form.
First, we need to load the recaptcha library. We’ll do so in the application layout, so that its 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:
- Define an onload callback function, and insert the reCAPTCHA resource.
- Leverage the reCAPTCHA
grecaptcha.render
function to configure reCAPTCHA. - 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 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:
- The ID of the element whose load will trigger this callback.
- 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
orfalse
.
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!