Both reCaptcha V2 and V3 in same Rails Project
Have you tried to use different versions of reCaptcha in a Rails project? I tried and seemed not possible using the recaptcha
gem. The reason is it only accepts one set of API credentials.
This is some thing I experienced. We were using reCaptcha V3 on a project and there was a need to implement a form that challenged the user. Given that reCaptcha V3 doesn't challenge the user and because V2 doesn't rank the user we decided to have both V2 and V3 side by side.
In a matter of hours I had a working solution. It does the job, but does need some refinement. If I have the time I will update this post with the updates I make.
We need a way to config the module with the site key and the secret key. This is the approach I take when I make a gem that requires configuration. While this is not a gem, its good to have a config like that since I planned to move it to a gem.
# frozen_string_literal: true
# app/system/invisible_recaptcha/config.rb
module InvisibleRecaptcha
module Config
class << self
attr_accessor :configuration
end
def self.configure
self.configuration ||= Configuration.new
yield(configuration)
end
class Configuration
attr_accessor :site_key, :secret_key
end
end
end
Then we need to config it with some thing like...
# config/initializers/recaptcha.rb
InvisibleRecaptcha::Config.configure do |config|
config.site_key = "<MY_SITE_KEY>"
config.secret_key = "<MY_SECRET>"
end
Then we need a way to call the Google API and get a response. This is a HTML wrapper. Note that we are going to get the configuration (secret) from the config instance. We are also using a Response
class which will be defined next.
# frozen_string_literal: true
module InvisibleRecaptcha
# HTTP wrapper for the API
class HTTP
ENDPOINT = "https://www.google.com/recaptcha/api".freeze
include HTTParty
base_uri ENDPOINT
attr_accessor :token, :ip
def initialize(token, ip = null)
@token = token
@ip = ip
end
def check
response = self.class.post "/siteverify", options
body = JSON.parse(response.body)
@response = InvisibleRecaptcha::Response.fromHTTPResponse(body)
end
def options
options = {}
options[:body] = {
secret: InvisibleRecaptcha::Config.configuration.secret_key,
response: token,
remoteip: ip
}
options
end
def self.check(response, ip = null)
new(response, ip).check
end
end
end
# frozen_string_literal: true
module InvisibleRecaptcha
class Response
ERROR_CODES = {
missing_input_secret: "The secret parameter is missing",
invalid_input_secret: "The secret parameter is invalid of malformed",
missing_input_response: "The response parameter is missing",
invalid_input_response: "The response parameter is invalid or malformed",
bad_request: "The request is invalid or malformed",
timeout_or_duplicate: "The response is no longer valid: either too old or has been used previously"
}.freeze
attr_accessor :success, :challenge_time_stamp, :hostname, :error_codes
alias success? success
def initialize(success:, challenge_time_stamp:, hostname:, error_codes:)
@success = success
@challenge_time_stamp = challenge_time_stamp
@hostname = hostname
@error_codes = error_codes
end
def error_messages
error_codes.map { |code| ERROR_CODES[code] }
end
def self.fromHTTPResponse(response)
error_codes = if response["error-codes"]
response["error-codes"].map { |code| code.gsub("-", "_") }
else
[]
end
new(
success: response["success"],
challenge_time_stamp: response["challenge_ts"],
hostname: response["hostname"],
error_codes: error_codes
)
end
def success?
@success.to_b
end
end
end
There are 2 modules that are to be included in application_controller.rb
and application_helper.rb
.
# frozen_string_literal: true
# app/system/invisible_recaptcha/extensions/controller.rb
module InvisibleRecaptcha
module Extensions
module Controller
def verify_invisible_recaptcha
token = params["g-recaptcha-response"]
@invisible_recaptcha_response = InvisibleRecaptcha::HTTP.check(token, request.remote_ip)
@invisible_recaptcha_response.success?
end
end
end
end
# frozen_string_literal: true
# apps/system/invisible_recaptcha/extensions/helper.rb
module InvisibleRecaptcha
module Extensions
module Helper
def recaptcha_v2_invisible(form, text, options = {})
klass = options.delete(:class)
site_key = InvisibleRecaptcha::Config.configuration.site_key
element = form.submit(text, class: "#{klass} g-recaptcha", data: { sitekey: site_key, callback: "InvisibleRecaptchaSubmit" })
script = recaptcha_v2_invisible_script
"#{script}\n#{element}".html_safe
end
private
def recaptcha_v2_invisible_script
<<-HTML
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<script>
var InvisibleRecaptchaSubmit = function() {
var button = $('.g-recaptcha');
var form = button.closest('form');
form.submit();
}
</script>
HTML
end
end
end
end
Then in the views you can use it like:
= form_for @user, url: users_path, method: :post, class: 'registration-form' do |f|
= f.invisible_captcha :name
// more form fields
.form-actions
= recaptcha_v2_invisible(f, 'Create Account', class: 'btn btn-primary pull-right')
About the Author
Ziyan Junaideen -
Ziyan is an expert Ruby on Rails web developer with 8 years of experience specializing in SaaS applications. He spends his free time he writes blogs, drawing on his iPad, shoots photos.