Adding reCaptcha v3 to a Rails app without a gem
Tags: rails, recaptcha, beginners
Published: 2019-06-04 23:58:13

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](https://github.com/ambethia/recaptcha), 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](https://github.com/igorkasyanchuk/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](https://g.co/recaptcha/v3).

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

```yaml
# config/credentials.yml.enc

recaptcha_site_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
recaptcha_secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

## 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`:

```ruby
# 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
```

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": [...]
}
```

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`:

```ruby
# 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
```

`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](https://apidock.com/rails/ActionView/Helpers/RawOutputHelper/raw)). The `%Q{}` acts like a double-quoted string, which allows you to also interpolate variables like the `#{site_key}`. (see [doc](https://docs.ruby-lang.org/en/2.0.0/syntax/literals_rdoc.html#label-Strings)). 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:

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

<head>
  # ...other tags

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

# ...more code
```

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

```erb
# 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 %>
```

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:

```erb
# 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 %>
```

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:

```ruby
# 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
```

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

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

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.

![](https://thepracticaldev.s3.amazonaws.com/i/hybuwh9zf357f4z4eaei.png)

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

```ruby
# 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
```

Happy Coding!

## Reference

- [Google reCaptcha v3 documentation](https://developers.google.com/recaptcha/docs/v3)
- [new_google_recaptcha gem documentation](https://github.com/igorkasyanchuk/new_google_recaptcha)
- [Japanese article on implementing reCaptcha into Rails](http://kenzo0107.hatenablog.com/entry/2019/02/17/000850)