Adding reCaptcha v3 to a Rails app without a gem

Felice Forby - Jun 4 '19 - - Dev Community

This article goes over how you can add Google reCaptcha v3 to a Rails app and use it to verify a form submission.

I couldn't find any other how-to articles in English on how to specifically add the new v3 to Rails except for using gems. This is a DIY explanation on how you can do it without using a gem.

At first, I had tried the recaptcha gem, but found the implementation difficult and kept getting errors, not to mention I didn't need all the functionality it provided.

The new_google_recaptcha on the other hand is quite simple and offers easy-to-follow documentation, so it could be a good alternative to doing it DIY.

In fact, I used some of the same techniques as the new_google_recaptcha for this article as well as some I found in a nicely written how-to article in Japanese (see references below). I also tried to add plenty of explanation for beginners (myself included!), so you know what's going on in the code.

Let's get started!

Getting Started

Register new reCaptcha keys for your site if you haven't already (you cannot use keys from reCaptcha v2). You can do so at the following link: https://g.co/recaptcha/v3.

Add the keys to your credentials.yml.enc file (or other secrets file if you're using something else):

# config/credentials.yml.enc

recaptcha_site_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
recaptcha_secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

Method for Verifying reCaptcha Tokens

In the ApplicationController, add a method for verifying reCaptcha. You can also set a minimum score. Make sure you also require net/https:

# app/controllers/application_controller.rb

require 'net/https'

class ApplicationController < ActionController::Base
  RECAPTCHA_MINIMUM_SCORE = 0.5

  # ... other methods

  def verify_recaptcha?(token, recaptcha_action)
    secret_key = Rails.application.credentials.dig(:recaptcha_secret_key)

    uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}")
    response = Net::HTTP.get_response(uri)
    json = JSON.parse(response.body)
    json['success'] && json['score'] > RECAPTCHA_MINIMUM_SCORE && json['action'] == recaptcha_action
  end
end
Enter fullscreen mode Exit fullscreen mode

The verify_recaptcha? method is sending out a verification request to the Google's reCaptcha api (https://www.google.com/recaptcha/api/siteverify) with the required parameters of secret and response (the ?secret=#{secret_key}&response=#{token} attached to the end of the uri). The token will be passed in later when adding the reCaptcha JavaScript.

Google will send back a response that looks like this:

{
  "success": true|false,      // whether this request was a valid reCAPTCHA token for your site
  "score": number             // the score for this request (0.0 - 1.0)
  "action": string            // the action name for this request (important to verify)
  "challenge_ts": timestamp,
  "hostname": string,         // the hostname of the site where the reCAPTCHA was solved
  "error-codes": [...]
}
Enter fullscreen mode Exit fullscreen mode

In the above code, the JSON is parsed with json = JSON.parse(response.body) and then we are checking if success is true, if the score satisfies our minimum score, and if the action matches the action we want. If all of these tests pass, verify_recaptcha? will return true.

Helper Methods for the reCaptcha JavaScript

Next, add a couple of helpers that will include the reCaptcha JavaScript code to the ApplicationHelper:

# app/helpers/application_helper.rb

module ApplicationHelper
  RECAPTCHA_SITE_KEY = Rails.application.credentials.dig(:recaptcha_site_key)

  def include_recaptcha_js
    raw %Q{
      <script src="https://www.google.com/recaptcha/api.js?render=#{RECAPTCHA_SITE_KEY}"></script>
    }
  end

  def recaptcha_execute(action)
    id = "recaptcha_token_#{SecureRandom.hex(10)}"

    raw %Q{
      <input name="recaptcha_token" type="hidden" id="#{id}"/>
      <script>
        grecaptcha.ready(function() {
          grecaptcha.execute('#{RECAPTCHA_SITE_KEY}', {action: '#{action}'}).then(function(token) {
            document.getElementById("#{id}").value = token;
          });
        });
      </script>
    }
  end
end
Enter fullscreen mode Exit fullscreen mode

include_recaptcha_js is the basic JavaScript for reCaptcha. We will add it into our applications <head> tag later. recaptcha_execute(action) is used to execute reCaptcha for different actions on the site. Specifically, we'll be using it to verify a form submission, so it includes code that adds a hidden field for the reCaptcha token. The token will get sent to controller later and verified with the verify_recaptcha? method we added earlier.

In the ApplicationHelper code above, the raw method allows you to output a string without Rails escaping all the tags (see doc). The %Q{} acts like a double-quoted string, which allows you to also interpolate variables like the #{site_key}. (see doc). It just makes it easier to write out the string.

Adding reCaptcha to the View Files

Let's set up our views. In the application.html.erb file, add a yield method in the head tag that will allow us to insert the reCaptcha JavaScript when necessary:

# app/views/layouts/application.html.erb

<head>
  # ...other tags

  <%= yield :recaptcha_js %>
</head>

# ...more code
Enter fullscreen mode Exit fullscreen mode

Next, we'll use reCaptcha to verify the submission of a basic order/contact form. Let's say we have the following form:

# app/views/orders/_form.html.erb

<%= form_for @order do |f| %>
  <%= render partial: 'shared/error_messages', locals: { current_object: @order } %>

  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>
  <div class="form-group">
    <%= f.label :email %>
    <%= f.text_field :email %>
  </div>
  <div class="form-group">
    <%= f.label :address %>
    <%= f.text_field :address, placeholder: true %>
  </div>
  <div class="form-group">
    <%= f.label :phone %>
    <%= f.text_field :phone %>
  </div>
  <div class="form-group">
    <%= f.label :message %>
    <%= f.text_area :message, rows: 10, placeholder: true %>
  </div>
  <%= f.submit t('orders.form.submit'), id: "submit" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Above the form, include the reCaptcha JavaScript using content_for so that it gets added to the yield in our <head> tag. We'll also verify that the submitter is not a bot by including the recaptcha_execute Javascript at the end of the form:

# app/views/orders/_form.html.erb

<%= content_for :recaptcha_js do %>
  <%= include_recaptcha_js %>
<% end %>

<%= form_for @order do |f| %>
  <%= render partial: 'shared/error_messages', locals: { current_object: @order } %>

  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>
  <div class="form-group">
    <%= f.label :email %>
    <%= f.text_field :email %>
  </div>
  <div class="form-group">
    <%= f.label :address %>
    <%= f.text_field :address, placeholder: true %>
  </div>
  <div class="form-group">
    <%= f.label :phone %>
    <%= f.text_field :phone %>
  </div>
  <div class="form-group">
    <%= f.label :message %>
    <%= f.text_area :message, rows: 10, placeholder: true %>
  </div>
  <%= f.submit t('orders.form.submit'), id: "submit" %>

  # Let's name the action 'order' since it submits an order
  <%= recaptcha_execute('order') %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

The recaptcha_excute('order') will spit out the reCaptcha execute JavaScipt and when the form is submitted, it will send the token to our controller so we can verify it.

Verifying the Submitted reCaptcha Token in the Controller

In the controller (in this case, the OrdersController), the create action handles the form submission:

# app/controllers/orders_controller.rb

class OrdersController < ApplicationController

  def create
    @order = Order.new(order_params)

    if @order.save
      OrderMailer.with(order: @order).new_order_email.deliver_later

      flash[:success] = t('flash.order.success')
      redirect_to root_path
    else
      flash.now[:error] = t('flash.order.error_html')
      render 'home/index'
    end
  end

  # other actions...

  private

  def order_params
    params.require(:order).permit(:name, :email, :address, :phone, :message)
  end
end
Enter fullscreen mode Exit fullscreen mode

We want to first verify if the submission is valid by using our verify_recaptcha? method:

unless verify_recaptcha?(params[:recaptcha_token], 'order')
  flash.now[:error] = "reCAPTCHA Authorization Failed. Please try again later."
  return render :new
end
Enter fullscreen mode Exit fullscreen mode

The form has sent the token from Google via the hidden input we added to the form (using the recaptcha_execute helper), so it is available in the params hash. The token and the action to check ('order') gets passed in our verification method like so: verify_recaptcha?(params[:recaptcha_token], 'order').

If everything is okay, the order is submitted. If not, the suspicious user is sent back to the form with an error message.

Note, if you don't add the return before the render :new here, you'll get a AbstractController::DoubleRenderError, because Rails will try to continue to execute the rest of the code in the action.

Here is the above code inserted into the controller's create action:

# app/controllers/orders_controller.rb

class OrdersController < ApplicationController

  def create
    @order = Order.new(order_params)

    unless verify_recaptcha?(params[:recaptcha_token], 'order')
      flash.now[:error] = t('recaptcha.errors.verification_failed')
      return render 'home/index'
    end

    if @order.save
      OrderMailer.with(order: @order).new_order_email.deliver_later

      flash[:success] = t('flash.order.success')
      redirect_to root_path
    else
      flash.now[:error] = t('flash.order.error_html')
      render 'home/index'
    end
  end

  private

  def order_params
    params.require(:order).permit(:name, :email, :address, :phone, :message)
  end
end
Enter fullscreen mode Exit fullscreen mode

Happy Coding!

Reference

. . . . . . . . . . . . . . . . . . . . . . . . .