DEV Community

Felice Forby
Felice Forby

Posted on • Edited on

Adding reCaptcha v3 to a Rails app without a gem

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

Top comments (20)

Collapse
 
storrence88 profile image
Steven Torrence

Did you run into any issues with Turbolinks doing it this way? I noticed the page with my form will only load the recaptcha if I refresh the page.

I tried wrapping the grecaptcha.ready function in a document.on(turbolinks:load) like this:

<script>
  $(document).on('turbolinks:load', function() {
      grecaptcha.ready(function() {
          grecaptcha.execute('#{ENV['RECAPTCHA_SITE_KEY']}', {action: '#{action}'}).then(function(token) {
            document.getElementById("#{id}").value = token;
          });
       });
   });
</script>
Enter fullscreen mode Exit fullscreen mode

but to no avail. Any suggestions?

Collapse
 
sammymhowe profile image
Samantha Howe

Hey did you ever figure this out? Running into Uncaught ReferenceError: grecaptcha is not defined and it's referencing a lot of Turbolinks links.

Collapse
 
storrence88 profile image
Steven Torrence

Hey! Sorry for the late reply. It's been awhile but I think I ended up disabling turbolinks for that particular page and the code ran fine after that.

Check this out: github.com/turbolinks/turbolinks#d...

Hope that helps!

Thread Thread
 
sammymhowe profile image
Samantha Howe

Ahh okay! I'll try that out, thank you so much!

Collapse
 
morinoko profile image
Felice Forby

Hey Steven, I did not have any issues with turbolinks doing it the way I wrote in the blog post. I unfortunately haven't tried it with a document.on(turbolinks:load) wrapper.

Collapse
 
rcdbeato profile image
Ricardo Beato Jr

The following line makes a GET request, right? Shouldn't it be a POST?

Net::HTTP.get_response(uri)

I'm sorry if the question doesn't make sense, I'm a newbie, feeling a bit lost...
Thanks again for the amazing post, though, very easy to follow!

Collapse
 
morinoko profile image
Felice Forby

Hmm, it does look like the Google documentation says to make a POST request...

Honestly, when I wrote this (and still now), I had a really hard time understanding how the ReCaptcha worked, so I had to reference some other articles. Those articles all used Net::HTTP.get_response(uri) to get back the verification response.

You could try to make it a POST request instead and see how it works. I haven't tried it myself yet!

Collapse
 
ben_stegeman_6497b156504d profile image
Ben Stegeman • Edited

I'm implementing a reCAPTCHA currently. I believe it is supposed to be a POST request.

In fact, it's a little insecure to pass the secret key (and the token to a lesser extent) via a GET request. The query parameters at the end of the URL are encrypted in transit (so long as HTTPS is used,) but they can still show up in server logs, etc.

Doesn't apply here, but those query parameters also show up in browser history! GET should never be used to transmit sensitive information over the web.

Collapse
 
benjp profile image
Ben

Hi Felice, great article!

I'm handling my forms with ajax (remote: true) and I'm struggling to figure out how I can reset the recaptcha after a successful post. Any help would be appreciated :)

Thanks,
Ben

Collapse
 
morinoko profile image
Felice Forby

Hi Ben, thanks for reading!

Unfortunately, I've never done the resetting with ajax myself, so I can't give you any tips from my own experience. I was just checking out some other blogs and the google documentation, though, and you might be able to use the reCaptcha javascript api and reset it with grecaptcha.reset(widgetId);. This blog post mentions it but not sure if it works.

Collapse
 
jeremylopez profile image
Jeremy Lopez

I know this is a little late but I just had to solve this problem so hopefully my response can help someone else. Basically, since my form is a create action, I edited my create.js file to update the token on failed submission. Here's how:

Here's a portion of my form where I render a partial that include my captcha tag:

...

<div class="col-12">
  <div class="form-group captcha captcha-lead_creation_modal">
    <%= render "potential_clients/captcha_field", action: 'lead_creation_modal' %>
  </div>
</div>

<div class="col-lg-12 d-flex m-t-20">
  <%= f.submit 'Request More Information', class: "btn btn-md btn-block btn-info-gradiant" %>
</div>

Here's the complete partial: potential_clients/captcha_field:

<%= recaptcha_execute(action) %>

Finally, in my create.js I do the following:

// other error handling / success code goes here

$(".captcha-lead_creation_modal").html("<%= j render 'potential_clients/captcha_field', action: 'lead_creation_modal' %>")

This will re-render the portion of the form with the token and since it hits the helper function, a new token is generated!

Collapse
 
morinoko profile image
Felice Forby

Nice! Thank you so much for sharing your solution!

Collapse
 
letalone profile image
let-alone

First of all
thanks!! for helpful article for google reCaprcha v3 for rails..

I need some help to apply of this document...

rails version of my system is 4.1.11 ( because app which I use, can run under rails 4.xx only)

So I cannot use credentials.yml.enc file

Instead of credentials.yml.enc I should use another secret file (for example , secret_key_base )

But I do not know how to change the line to make the program work normally in ApplicationController.

I'm sorry, please teach me.

Collapse
 
morinoko profile image
Felice Forby

Hi there! Thank for reading!

Well, secrets from Rails' credential system are read with code that look like this (for example): Rails.application.credentials.dig(:recaptcha_secret_key). So, anywhere you see Rails.applications.credentials..., you need to replace it with how the other secret file like secret_key_base reads those variables. I'm not exactly sure how to do it for Rails 4 though, so you would need to research about that.

I have also used the dotenv gem before, which is very easy to use. You learn more about dotenv here. Basically, you make a file named .env and register any secret keys you need there. Then read them with something like ENV['SECRET_KEY'].

Hope that helps a little bit >< Sorry I don't know enough about the Rails 4 way to do it!

Collapse
 
cbillen profile image
Christian Billen

Anyone noticing recaptcha failures with this approach? the problem is a reCaptcha v3 will time out after 2 minutes, so if your users take longer to fill in the form the recaptcha json will return error-codes timeout-or-duplicate with a score of 0. It might be better to call recaptcha_execute on form submit to avoid this problem.

Collapse
 
jasonrhalpern profile image
jasonrhalpern

This article was really helpful so thanks for writing it up.

To address this comment, when I get back error-codes timeout-or-duplicate I am not receiving any score in that response (and success is false). I only get a score in the response when success is set to true, so I think the fact that he put json['success'] && json['score'] will address this correctly.

One thing I did was to add error handling with a begin/rescue in verify_recaptcha? because there are a couple things in that method that can blow up.

Collapse
 
nacengineer profile image
David Southard

Great article as I found the google Docs extremely lacking on how to set this up for any site much less a Rails site. They make a lot of assumptions that you'll just get the part they're completely glossing over, e.g. the hidden field and token assignment.

One mistake I think I found though is in the method recaptcha_execute you have a reference to RECAPTCHA_SITE_KEY which I'm guessing is actually supposed to be ENV['RECAPTCHA_SITE_KEY'] or a defined constant. This might trip up some people. :)

Collapse
 
morinoko profile image
Felice Forby

Yikes, nice catch! It was supposed to be a constant above the two methods. Now fixed! Thanks for pointing that out.

I had a hard time with the Google documentation too, which is a big reason why I wrote this post! Thank you for reading :D

Collapse
 
cardyard profile image
Mike Hayman

Top drawer Felice, thanks so much!

Collapse
 
rcdbeato profile image
Ricardo Beato Jr

Great post, simple to follow!