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
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
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
:
# 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). 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
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 %>
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 %>
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
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
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
Happy Coding!