Skip to content

Recaptcha with Turbo and Stimulus

Dong Liu edited this page Oct 15, 2023 · 7 revisions

If you would like to make Recaptcha work with Turbo and Stimulus, you'll have to perform some additional steps.

Step 0: Obtain Recaptcha keys

You'll need keys for both Recaptcha v3 and v2 so go ahead and grab those if you haven't already done so.

Step 1: Display Recaptcha v3 in your form

By default we'll use Recaptcha v3. It must be wrapped in a turbo frame (please note that your form can also be wrapped):

<%= turbo_frame_tag user, target: '_top' do %>
  <%= form_with model: user do |f| %>
    <!-- your form fields here... -->

    <%= turbo_frame_tag 'recaptcha' do %>
      <div class="mb-3 row">
        <%= recaptcha_v3 action: 'signup', site_key: ENV['RECAPTCHA_KEY_V3'], turbolinks: true %>
      </div>
    <% end %>
    
    <%= f.submit 'Submit' %>
  <% end %>
<% end %>

Notice the usage of the turbolinks option which is mandatory in this setup.

Step 2: Verify Recaptcha v3 in your controller

Now let's perform verification:

def create
  @user = User.new user_params
  @user.validate # this line will validate the user even if Recaptcha failed. This way we will present all potential validation errors right away

  check = verify_recaptcha action: 'signup', minimum_score: 0.7, secret_key: ENV['RECAPTCHA_SECRET_V3']

  if check && @user.save
     # everything is great, you can now let the user in and redirect them somewhere
  else
    render :new # if something goes wrong, we'll re-render the form
  end
end

Please note that action should be the same as the one provided in the view.

Step 3: Display Recaptcha v2 if v3 check failed

If Recaptcha v3 check failed, we are going to display a regular "I'm not a robot" checkbox using Recaptcha v2. Therefore, create a new view called new.turbo_stream.erb:

<%= turbo_stream.replace 'recaptcha' do %>
  <div class="mb-3 row"
    id='recaptchaV2'
    data-controller='recaptcha-v2'
    data-recaptcha-v2-site-key-value="<%= ENV['RECAPTCHA_KEY'] %>"></div>
<% end %>

We are replacing the old recaptcha with a new one. Here you can also re-render the actual form thus displaying validation errors:

<%= turbo_stream.replace dom_id(@user), partial: 'users/form', locals: {user: @user} %>

<%= turbo_stream.replace 'recaptcha' do %>
  <div class="mb-3 row"
    id='recaptchaV2'
    data-controller='recaptcha-v2'
    data-recaptcha-v2-site-key-value="<%= ENV['RECAPTCHA_KEY'] %>"></div>
<% end %>

You can also display flash messages here and perform other actions as needed.

At this point we are simply displaying a placeholder for our new captcha. It will be processed by Stimulus in the next step.

Step 4: Use Stimulus to process Recaptcha v2

Now create a new Stimulus controller:

// recaptcha_v2_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { siteKey: String }

  initialize() {
    grecaptcha.render("recaptchaV2", { sitekey: this.siteKeyValue } )
  }
}

At this point we actually render a new captcha. Don't forget to properly register your controller inside the index.js file.

Step 5: Validate Recaptcha v2

Now modify your controller:

def create
  @user = User.new user_params

  check = (verify_recaptcha action: 'signup', minimum_score: 0.7, secret_key: ENV['RECAPTCHA_SECRET_V3']) ||
    (verify_recaptcha model: @user, secret_key: ENV['RECAPTCHA_SECRET'])

  if check && @user.save
    # Everything is good
  else
    @user.validate # add any other validation errors
   
    # @user.validate generates a new errors, so that recaptcha error message cannot be seen.
    @user.errors.add(:base, t('recaptcha.errors.verification_failed')) unless check 
    
    render :new
  end
end

What about Devise?

This guide is partially based on this tutorial at dev.to which explains how to get Recaptcha working with Devise (the overall approach is the same).