Catalogue
Adding reCAPTCHA v3 to Rails for Bot Protection

Adding reCAPTCHA v3 to Rails for Bot Protection

🌐 日本語で読む

Overview

A web service I built with Rails was being hit by bot attacks on a regular basis, so I added reCAPTCHA v3 to the contact form.

Why reCAPTCHA v3 and not v2?

With v2, after you tick the I'm not a robot checkbox, there is a step that makes you pick images.

For example, if you are asked “Which of these show a billboard?”, the psychological burden is high (“How much of it counts as the billboard?”), and there is a chance the user will abandon the form.

What’s nice about v3?

v3 *1 scores user behavior on the page where it is installed and decides whether the visitor is a bot.

The more traffic it gets, the more accurate it becomes.

It places no burden at all on legitimate (non-bot) users while still blocking bots — it really makes you feel how far the world has come.

Is there a gem?

I didn’t use a gem this time.

The reasons were as follows:

  • gem 'recaptcha' does not support v3.
  • gem 'new_google_recaptcha' does support v3, but it doesn’t return the score, which makes it hard to test.

There may be others out there, but I couldn’t find one at the time of writing.

First, issue a reCAPTCHA v3 key

Go to the reCAPTCHA console below and issue a key.

https://g.co/recaptcha/v3

Select v3 and register the domain you are introducing it to.*2

Save the issued site key and secret key.

  • Site key
    • The key needed to obtain a token when a user accesses the site. It is fine to expose this to the public.
  • Secret key
    • The key needed when querying Google based on the token. Treat this as confidential information.

Implementation on the Rails side

This assumes Rails >= 5.2.

config/credentials.yml.enc

recaptcha:
  secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Store the secret in your confidential credentials.

app/controllers/application_controller.rb

require 'net/http'
require 'uri'

class ApplicationController < ActionController::Base
...
  RECAPTCHA_MINIMUM_SCORE = 0.5
  RECAPTCHA_ACTION = 'homepage'
...
  def verify_recaptcha?(token)
    secret_key = Rails.application.credentials.recaptcha[:secret_key]
    uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}")
    r = Net::HTTP.get_response(uri)
    j = JSON.parse(r.body)
    j['success'] && j['score'] > RECAPTCHA_MINIMUM_SCORE && j['action'] == RECAPTCHA_ACTION
  end
end

As a shared method, I define the recaptcha authentication method `verify_recaptcha?`.

Here, a score of 0.5 or below is treated as a bot.

If you operate the page normally, you'll easily exceed that value.

config/locales/en.yml

en:
  recaptcha:
    errors:
      verification_failed: 'reCAPTCHA Authorization Failed. Please try again later.'

This is the locale `en` configuration.

config/locales/ja.yml

ja:
  recaptcha:
    errors:
      verification_failed: 'reCAPTCHA 認証失敗しました。しばらくしてからもう一度お試しください。'

This is the locale `ja` configuration.

app/controllers/hoges_controller.rb

class HogesController < ApplicationController
  def new; end

  def create
    unless verify_recaptcha?(params[:recaptcha_token])
      flash.now[:recaptcha_error] = I18n.t('recaptcha.errors.verification_failed')
      return render action: :new
    end

    # something to do

    redirect_to hoge_finish_path
  end

  def finish; end
end

The flow posts from new to create, runs the bot check via reCAPTCHA, and then

  • OK → proceed to finish
  • NG → return to new

is the design.

app/views/hoges/new.html.erb

<% if flash[:recaptcha_error] %>
<div class="text">
  <p><spacn class="error"><%= flash[:recaptcha_error] %></span></p>
</div>
<% end %>

<%= form_tag({action: :create}, {method: :post}) do %>
...
  <input id="recaptcha_token" name="recaptcha_token" type="hidden"/>
  <%= submit_tag "送信する", :class => "submit-recaptcha btn", :disabled => true %>
<% end %>

<script src="https://www.google.com/recaptcha/api.js?render=<%= Settings.recaptcha.site_key %>&ver=3.0"></script>
<script>
grecaptcha.ready(function() {
  grecaptcha.execute('<%= Settings.recaptcha.site_key %>', {action: 'homepage'}).then(function(token) {
    $('#recaptcha_token').val(token);
    $('.submit-recaptcha').prop('disabled', false);
  });
});
</script>
Displaying the error message
<% if flash[:recaptcha_error] %>
<div class="text">
  <p><spacn class="error"><%= flash[:recaptcha_error] %></span></p>
</div>
<% end %>
Add the following name=recaptcha_token input tag inside &lt;form&gt; ~ &lt;/form&gt;.
<input id="recaptcha_token" name="recaptcha_token" type="hidden"/>
Embed a script to obtain the reCAPTCHA token on page access.
<script src="https://www.google.com/recaptcha/api.js?render=<%= Settings.recaptcha.site_key %>&ver=3.0"></script>
<script>
grecaptcha.ready(function() {
  grecaptcha.execute('<%= Settings.recaptcha.site_key %>', {action: 'homepage'}).then(function(token) {
    $('#recaptcha_token').val(token);
    $('.submit-recaptcha').prop('disabled', false);
  });
});
</script>

When the reCAPTCHA token is obtained successfully, the following are executed.

  • Set the token as the value of the id="recaptcha_token" input tag
  • Enable the submit button

Regarding `<%= Settings.recaptcha.site_key %>`
this is configured on the assumption that `gem 'settingslogic'` is installed.

If you haven't installed it, and you just want to try out the process quickly, replace `<%= Settings.recaptcha.site_key %>` with the site key you obtained.*3

That completes the setup.

Try accessing the page

The reCAPTCHA badge will now always be displayed in the bottom-right corner of the page.

Viewing the aggregated information

When you look at the reCAPTCHA console, you'll probably see a display like the one below, with the aggregated information not yet reflected.

After a while, a graph like the following will start to be displayed.

Caveat

For example, if you access the site frequently for testing from a fixed IP such as an internal corporate IP, you'll be treated as a bot.

There is no IP whitelist on the reCAPTCHA side, so in that case you'll need to build an allowed-IP list on the Rails side.

That's all.
I hope you find it helpful.

*1:The latest version as of February 2019

*2:You can register multiple domains. If you want to change the aggregation or bot-prevention behavior per domain, issue keys individually. Also, issuing separate keys for RAILS_ENV = production and the rest is recommended since it avoids impacting production.

*3:As mentioned earlier, managing the site key through some mechanism rather than specifying it directly is recommended.

Adding reCAPTCHA v3 to Rails for Bot Protection

https://kenzo0107.github.io/en/2019/02/17/rails-recaptcha-v3-bot/

Author

Kenzo Tanaka

Posted on

2019-02-17

Licensed under