reCAPTCHA v3 on Rails

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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s