Back to home

Devise Part 6: Adding Recaptcha To Prevent Fraud And Abuse

Published: Mar 8, 2022

Last updated: Mar 8, 2022

Part six of the series will take all the work that we have done so far and add reCAPTCHA to the sign up and login forms.

Source code can be found here.

Prerequisites

  • Catch up with previous parts in this series Devise with Ruby on Rails 7.
  • Worth reading a little about what reCAPTCHA is and how it works.

Getting started

We will be working from the source code found here.

# Clone and change into the project $ git clone https://github.com/okeeffed/demo-rails-7-with-devise-series $ cd demo-rails-7-with-devise-series # Continue from part 4 - warning that Redis required $ git checkout 5-github-oauth # Add recaptcha gem $ bundler add recaptcha

The ambethia/recaptcha Gem is required for this part and does most of the heavy lifting.

Updating our sign up view

We need to add some tags to our sign up view. I have commented the new lines that we are adding below.

In app/views/devise/registrations/new.html.erb:

<h2>Sign up</h2> <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { data: { turbo: false} }) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> </div> <div class="field"> <%= f.label :password %> <% if @minimum_password_length %> <em>(<%= @minimum_password_length %> characters minimum)</em> <% end %><br /> <%= f.password_field :password, autocomplete: "new-password" %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation, autocomplete: "new-password" %> </div> <div class="actions"> <%= f.submit "Sign up" %> </div> <%# START add reCAPTCHA %> <%= flash[:recaptcha_error] %> <%= recaptcha_tags %> <%# END add reCAPTCHA %> <% end %> <%= render "devise/shared/links" %>

This handles the basic setup for the sign up form.

Adding reCAPTCHA to the sign in form

In a similar manner, we need to update our sign in view.

app/views/devise/sessions/new.html.erb should look like the following:

<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div> <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg" alt="Workflow"> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> <p class="mt-2 text-center text-sm text-gray-600"> Or <%= link_to "sign up", new_registration_path(resource_name), class: "font-medium text-indigo-600 hover:text-indigo-500" %> </p> <% end %> </div> <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { data: { turbo: false} }) do |f| %> <div class="mt-8 space-y-6"> <input type="hidden" name="remember" value="true"> <div class="rounded-md shadow-sm -space-y-px"> <div> <%= f.label :email, class: "sr-only", for: "email-address" %> <%= f.email_field :email, id: "email-address", autofocus: true, autocomplete: "email", placeholder: "Email address", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %> </div> <div> <%= f.label :password, for: "password", class: "sr-only" %> <%= f.password_field :password, id: "password", autocomplete: "current-password", placeholder: "Password", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %> </div> </div> <% if devise_mapping.rememberable? %> <div class="flex items-center justify-between"> <div class="field flex items-center"> <%= f.check_box :remember_me, class: "h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" %> <%= f.label :remember_me, class: "ml-2 block text-sm text-gray-900" %> </div> <div class="text-sm"> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%= link_to "Forgot your password?", new_password_path(resource_name), class: "font-medium text-indigo-600 hover:text-indigo-500" %> <% end %> </div> </div> <% end %> <div class="actions"> <%= f.submit "Log in", class: "actions group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> </div> <%# START add reCAPTCHA %> <%= flash[:recaptcha_error] %> <%= recaptcha_tags %> <%# END add RECAPTCHA %> </div> <% end %> <%# ADD HERE %> <%- if devise_mapping.omniauthable? %> <%- resource_class.omniauth_providers.each do |provider| %> <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post, "data-turbo": false %> <% end %> <% end %> <%# END ADD HERE %> </div> </div>

Add this point, both of our views are updated.

Note that for both forms, we needed to set html.data.turbo to false.

Setting up test keys for reCAPTCHA

Next, we need to add some environment variables to our .env file.

We will use test keys provided by Google as outlined here

RECAPTCHA_SITE_KEY="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" RECAPTCHA_SECRET_KEY="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"

Generating the required Devise controllers

We need to generate our registrations and sessions controller for Devise at this point as per the Devise docs.

In the console:

$ bin/rails g devise:controllers users -c=registrations create app/controllers/users/registrations_controller.rb $ bin/rails g devise:controllers users -c=sessions create app/controllers/users/sessions_controller.rb

Then, we need to update the file app/controllers/users/registrations_controller.rb:

# frozen_string_literal: true class Users::RegistrationsController < Devise::RegistrationsController prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect. private def check_captcha return if verify_recaptcha # verify_recaptcha(action: 'signup') for v3 self.resource = resource_class.new sign_up_params resource.validate # Look for any other validation errors besides reCAPTCHA set_minimum_password_length respond_with_navigational(resource) do flash.discard(:recaptcha_error) # We need to discard flash to avoid showing it on the next page reload render :new end end end

Inside of the user sessions controller app/controllers/users/sessions_controller.rb, add the following:

# frozen_string_literal: true class Users::SessionsController < Devise::SessionsController prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect. private def check_captcha return if verify_recaptcha # verify_recaptcha(action: 'login') for v3 self.resource = resource_class.new sign_in_params respond_with_navigational(resource) do flash.discard(:recaptcha_error) # We need to discard flash to avoid showing it on the next page reload render :new end end end

The docs also take you through adding reCAPTCHA for the password reset page, but we will skip that part for now.

Configuring our router

Finally, we need to add our recently generated sessions and registrations controllers to the Devise router helper:

Rails.application.routes.draw do get 'session/index' devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', sessions: 'users/sessions', registrations: 'users/registrations' } # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html resources :session, only: [:index] resources :home, only: %i[index create] # Defines the root path route ("/") root 'home#index' end

Testing our application

At this point, we can boot the application back up with bin/dev and attempt to login at http://localhost:3000/users/sign_in. You will see reCAPTCHA load up on the sign in form:

Sign in with reCAPTCHA

Sign in with reCAPTCHA

First, attempt to sign in a user without selecting reCAPTCHA and you will get a failure message:

Failed sign in

Failed sign in

Signing in correctly will work as expected after clicking on the reCAPTCHA form.

Afterwards, you can also sign out and try it on the sign up form to see a similar setup for password registration.

Summary

Today's post demonstrated how to configure Devise with reCAPTCHA for Ruby on Rails 7.

It built on top of the work from the previous posts in the series.

In the next post in the series, we will look at how we can configure the devise_token_auth gem to enable token-based authentication for Rails JSON APIs.

Resources and further reading

Photo credit: davidclode

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

Devise Part 6: Adding Recaptcha To Prevent Fraud And Abuse

Introduction

Share this post