I implemented reCAPTCHA v3 on a Rails site recently. It was very straightforward, and I’m generally pleased with the outcome. Interestingly, the vast majority of spam requests seem to be made without having generated a reCAPTCHA token, suggesting that they are not even loading JavaScript. This points to a possible poor man’s approach for spam suppression: generate your CSRF token using JavaScript rather than emitting it directly in the form.
For better or worse, it is difficult to operate on the web today without JavaScript.
Version 3 of reCAPTCHA operates by capturing all user site activity, which is a privacy concern, but also allows it to function unobtrusively.
I defined several variables in my environment:
RECAPTCHA_CHECK = true
RECAPTCHA_SITE_KEY = 'xyz'
RECAPTCHA_SECRET_KEY = 'xyz'
I load the reCAPTCHA script on secondary pages of my signup site, but not the root page and not any internal pages:
<% if RECAPTCHA_CHECK -%>
<script src="https://www.google.com/recaptcha/api.js?render=<%= RECAPTCHA_SITE_KEY %>"></script>
<% end -%>
Then I attach an action to the signup form submission to generate the reCAPTCHA token and include it in the form data (the form is named signup_form
and has a hidden input named recaptcha
):
<% if RECAPTCHA_CHECK -%>
// Add reCAPTCHA token on form submission
$(function() {
$('#signup_form').submit(function(event) {
event.preventDefault();
$('#signup_form').off('submit');
grecaptcha.ready(function() {
grecaptcha.execute('<%= RECAPTCHA_SITE_KEY %>', {action: 'submit'}).then(function(token) {
$('#recaptcha').val(token);
$('#signup_form').submit();
});
});
});
});
<% end -%>
This site already has several conditions for trashing a suspicious signup request, with a simple error page that directs visitors to reach out to our administrators by email. I added a new condition to check the reCAPTCHA token:
if RECAPTCHA_CHECK
# Check reCAPTCHA
recap_uri = URI.parse('https://www.google.com/recaptcha/api/siteverify')
recap_params = { secret: RECAPTCHA_SECRET_KEY, response: params[:recaptcha] }
response = Net::HTTP.post_form(recap_uri, recap_params)
logger.info "Recaptcha result: " + response.code + " / " + response.body
response_json = JSON.load(response.body) if response.code == "200"
end
# Silently throttle requests that fail reCAPTCHA
if RECAPTCHA_CHECK && (response.code != "200" || !response_json['success'] || response_json['score'] < 0.5)
Mailer.exception_notification(Exception.new("reCAPTCHA throttled signup"), params).deliver_now
redirect_to :action => :thankyou
end
As you can see, for now I am operating with a threshold of 0.5, but I may adjust this over time. In my own testing, I generated a confidence value of 0.9.