Rails Devise and ReCaptcha with Hotwire (Turbo and Stimulus)
Tags: hotwire, turbo, stimulus, recaptcha
Published: 2021-06-10 13:37:50

## Introduction

Last month, I decided to install [Hotwire](https://hotwire.dev/) *(which is the combination of [Turbo](https://turbo.hotwire.dev/) and [Stimulus](https://stimulus.hotwire.dev/))* to my **Rails** app. I thought it would be easy and quick to make those upgrades but I kind of underestimated the task. 

**Stimulus** is really easy to learn if you already know javascript. **Turbo** is more abstract and can be difficult to fully understand how it works under the hood. In addition, **Turbo** is quite new *(it is the evolution of [Turbolinks](https://github.com/turbolinks/turbolinks))* so it is not really well documented as of June 2021.

One of the challenge I had to face is to make **Turbo** work with [Devise](https://github.com/heartcombo/devise) and to keep my [ReCaptcha](https://github.com/ambethia/recaptcha) strategy working *(First verify ReCaptcha v3 and then display ReCaptcha v2 if v3 failed)*. So let's dive in !

_For the following, I assume you have a functioning Rails app with the following Gems installed: Devise, Stimulus-rails, turbo-rails and recaptcha_

## Handling Devise error messages

If you installed Turbo successfully, then you should have noticed that when submitting your **Log in** form it now renders as `TURBO_STREAM` format _(this is because Turbo applies to forms and not only to links like Turbolinks did)_:

```server
Processing by PagesController#home as TURBO_STREAM

```
If you submit your **Log in** form with the correct information, everything should work as expected.

The problem is when you do not enter the correct information _(unknown email address, wrong password...)_, the error messages do not appear anymore and this is not what we want.
For fixing this part, I was largely inspired by this excellent [tutorial](https://gorails.com/episodes/devise-hotwire-turbo) from Chris Oliver.

### 1 - Add Turbo Stream to devise formats

First we need to add Turbo Stream into devise navigational formats list. In the _devise.rb_ file, uncomment the `config.navigational_formats` line and add the `turbo_stream` format:
```ruby
# devise.rb

config.navigational_formats = ['*/*', :html, :turbo_stream]
```

### 2 - Override Devise Respond method

In order for devise to display errors to the user when the authentication failed, we need to partially override the **respond** method of the `Devise::FailureApp` class by creating our own `TurboFailureApp`. This will "treat" the turbo stream like it would for an HTML request by redirecting back to the **Log in** page in case of failure. You can add this class directly on top of the _devise.rb_ file (out of the `devise.config` block) since we will probably never use this new class in another context:

```ruby
# devise.rb

class TurboFailureApp < Devise::FailureApp
  def respond
    if request_format == :turbo_stream
      redirect
    else
      super
    end
  end

  def skip_format?
    %w[html turbo_stream */*].include? request_format.to_s
  end
end
```

### 3 - Configure Warden

Finally, we need to tell [Warden](https://github.com/wardencommunity/warden/wiki/Overview) (The Rack-based middleware used by devise for authentication) to use our newly created class as the failure manager. In the _devise.rb_, uncomment the `config.warden` block and replace the inner content like this:

```ruby
# devise.rb

config.warden do |manager|
  manager.failure_app = TurboFailureApp
end

```
And that's it, you can try it out and should see Devise error messages appear on authentication failure.

## Handling ReCaptchas with Turbo and Stimulus

Now that devise authentication works correctly with **Turbo**, we want to implement a **ReCaptcha** protection to our form. The **ReCaptcha** strategy I use is quite classical as it consists of first rendering a **ReCaptcha v3** (which is invisible and based on requests liability scores), and in case the score is lower than what is expected, we render a **ReCaptcha v2** Checkbox so that the user can check it manually.

### 1 - Add the ReCaptcha V3 in your Log in form

Thanks to the _recaptcha_ Gem we have helpers method to simplify the process. We will also play along with **Turbo Frames** to handle html replacement when ReCaptcha v3 is not correctly verified. In our _sessions/new.html.erb_ file we can include the following code within the **Log in** form:

```erb
<!-- devise/sessions/new.html.erb -->

<%= turbo_frame_tag 'recaptchas' do %>
  <%= recaptcha_v3(action: 'login', site_key: ENV['RECAPTCHA_SITE_KEY_V3']) %>
<% end %>
```

### 2 - Implement a method to verify the ReCaptchas

In our **Sessions controller**, we need to implement a method that will allow the **ReCaptcha API** to verify the request. We will use a `prepend_before_action` callback on the create method, to verify the ReCaptchas requests even before devise user authentication.

```ruby
# users/sessions_controller.rb

class Users::SessionsController < Devise::SessionsController
  prepend_before_action :validate_recaptchas, only: [:create]

  protected

  def validate_recaptchas
    v3_verify = verify_recaptcha(action: 'login', 
                                 minimum_score: 0.9, 
                                 secret_key: ENV['RECAPTCHA_SECRET_KEY_V3'])
    return if v3_verify

    self.resource = resource_class.new sign_in_params
    respond_with_navigational(resource) { render :new }
  end
end

```

The code above will work if the request score is 0.9 or higher, but if it is lower, it will render an error because **Turbo** will look for a partial with the `turbo_stream` format. Plus we are only telling devise to render the form again without any improvement. We want to render the **ReCaptcha v2** so that the user is not stuck on the same form with a low request score again and again.

### 3 - Create a turbo_stream view for ReCaptcha v2

Then let's create a view for our ReCaptcha v2 injection into the form. Since we want to replace the ReCaptcha v3 with the ReCaptcha v2 we will use the Turbo Stream helper `turbo_stream.replace` to wrap the code we want to inject:
```erb
<!-- sessions/new.turbo_stream.erb -->

<%= turbo_stream.replace :recaptchas do %>
<% end %>
```

Note that the `:recaptchas` attribute is the same as the `turbo_frame_tag` _id_ we set in our _sessions/new.html.erb_ as a wrapper for our ReCaptcha v3. We will replace the content of the `turbo_frame_tag 'recaptchas'` with the content of our `turbo_stream`. Cool right ?

Now is the tricky part, you will probably be tempted to use the _recpatchas_ Gem helper method to render our ReCaptcha v2 checkbox like:
```erb
<!-- sessions/new.turbo_stream.erb -->
# Do not do that !

<%= turbo_stream.replace :recaptchas do %>
  <div class="mt-4">
    <%= recaptcha_tags(site_key: ENV['RECAPTCHA_SITE_KEY_V2']) %>
  </div>
<% end %>
```
This will not work because of **Turbo**. Indeed, since Turbo avoid the page refresh and is not evaluating inline scripts when rendering (It is a bug tracked here: [hotwired/turbo#186](https://github.com/hotwired/turbo/issues/186)), the **ReCaptcha API** will not be called again. We thus have to render the **ReCaptcha v2** explicitly and this is where **Stimulus** comes in !

In the `turbo_stream.replace` block from your _new.turbo_stream.erb_ file, insert an empty DIV with the id `recaptchaV2` and with the necessary attributes to be able to connect to our future Stimulus controller:

```erb
<!-- sessions/new.turbo_stream.erb -->

<%= turbo_stream.replace :recaptchas do %>
  <div class='mt-4' id='recaptchaV2' data-controller='load-recaptcha-v2' data-load-recaptcha-v2-site-key-value=<%= ENV['RECAPTCHA_SITE_KEY_V2'] %>></div>
<% end %>
```

### 4 - Render ReCaptcha v2 explicitly from Stimulus

Create a Stimulus controller called **load_recaptcha_v2_controller.js** _(It has to be the same name as the data-controller attribute from our `recaptchaV2` DIV)_. In this controller we will fetch our ReCaptcha v2 **sitekey** value defined in our `data-load-recaptcha-v2-site-key-value` attribute from our `recaptchaV2` DIV and then we will inject the **ReCaptcha V2** explicitely on initialization. So your _load_recaptcha_v2_ controller should look like this:

```javascript
// load_recaptcha_v2_controller.js

import { Controller } from "stimulus"

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

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

### 5 - Add the ReCaptcha v2 verification

We now need to add the **ReCaptcha v2** verification in our **Session controller**. To do so is really simple, just modify your `validate_recaptchas` method to include v2 validation like below:

```ruby
  def validate_recaptchas
    v3_verify = verify_recaptcha(action: 'login', 
                                 minimum_score: 0.9, 
                                 secret_key: ENV['RECAPTCHA_SECRET_KEY_V3'])
    v2_verify = verify_recaptcha(secret_key: ENV['RECAPTCHA_SECRET_KEY_V2'])
    return if v3_verify || v2_verify

    self.resource = resource_class.new sign_in_params
    respond_with_navigational(resource) { render :new }
  end
```

Now test it ! To verify if the **ReCaptcha v2** is correctly rendered, set the `minimum_score` of the  ReCaptcha v3 verification helper to **1** which will cause the v3 to fail everytime and this is what we want in order to test it. Of course do not forget to put this value back to normal afterwards !

### 6 - Configure Flash messages

In case of **ReCaptcha v3** validation failures, you probably want your user to know what is happening by displaying a **flash message** like "Please check the ReCaptcha to continue" or something like this.

Well, we need to make some small changes to the default flash messages settings because **Flash messages** will not work when Turbo (again !) do not redirect.

Go to your _application.html.erb_ file and wrap the Flash messages partial into a **Turbo Frame**:

```erb
<!-- application.html.erb -->

<%= turbo_frame_tag :flashes do %>
  <%= render 'shared/flashes' %>
<% end %>

```

Then all you have to do is **update** the Flash messages partial through a **Turbo Stream** in your _sessions/new.turbo_stream.erb_ file which now looks like this:

```erb
<%= turbo_stream.replace :recaptchas do %>
  <div class='mt-4' data-controller='load-recaptcha-v2' id='recaptchaV2' data-load-recaptcha-v2-site-key-value=<%= ENV['RECAPTCHA_SITE_KEY_V2'] %>></div>
<% end %>
<%= turbo_stream.update(:flashes, partial: 'shared/flashes', locals: { alert: 'Please check the ReCaptcha to continue' }) %>
```

And that is finally it ! You should have a functional Devise authentication form working with Turbo. 

I hope this helped and of course if you have some doubts or even better, if you have improvements or refactoring to propose please feel free to contact me !