Skip to content

Commit

Permalink
Add a Cloudflare Turnstill captcha to the signup form
Browse files Browse the repository at this point in the history
In the Postmark activity I've noticed a lot of "verify your email"
messages being sent and not actioned. Many of these are bouncing,
which hurts our email deliverability. Anecdotally more legitimate
emails from jam.coop are going to spam than before.

To mitigate this I've added a Cloudflare Turnstile[1] "captcha" to the
registration form to hopefully prevent these non-genuine
signups (which I assume are from scripts) from occuring.

I decided to use the rails-cloudflare-turnstile[2] gem to do the heavy
lifting here, primarily to avoid having to write the code that makes
the verfication request to the cloudflare API[3].

I've mostly followed the setup instructions for that gem with a couple
of changes:

1. I've disabled the gem-provided mock in favour of using Cloudflare's
development/testing tokens so that we can see the real capture
locally.
2. Because we use turbo I've had to do ensure that the cloudflare
script is always loaded and reloaded when the signup page is visited
or re-rendered after a validation error. Setting `data-turbo=false` on
the signup form ensures that it is submitted without turbo and
therefore releads the cloudflare JS widget on re-render. Including the
cloudflare script with the `data-turbo-track` and
`data-turbo-temporary` options ensures that it is reloaded if the user
navigates away from signup and back again.

I've also set the two new ENV variables using the credentials provided
by Cloudflare[4].

[1] https://developers.cloudflare.com/turnstile/
[2] https://github.com/instrumentl/rails-cloudflare-turnstile
[3] https://github.com/instrumentl/rails-cloudflare-turnstile/blob/main/lib/rails_cloudflare_turnstile/controller_helpers.rb
[4] https://dash.cloudflare.com/
  • Loading branch information
chrislo committed Jul 19, 2024
1 parent bfc6a04 commit 009ce06
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 3 deletions.
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ AWS_SECRET_ACCESS_KEY=
STRIPE_SECRET_KEY=
STRIPE_ENDPOINT_SECRET=
ROLLBAR_ACCESS_TOKEN=
ROLLBAR_POST_CLIENT_ITEM_ACCESS_TOKEN=
ROLLBAR_POST_CLIENT_ITEM_ACCESS_TOKEN=

# These keys always pass the registration turnstile check. Other keys
# are availble to test various failure scenarios
# https://developers.cloudflare.com/turnstile/troubleshooting/testing/
CLOUDFLARE_TURNSTILE_SITE_KEY=1x00000000000000000000AA
CLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ gem 'pundit', '~> 2.3'
gem 'rack-maintenance'
gem 'rails', '~> 7.1'
gem 'rails_autolink', '~> 1.1'
gem 'rails_cloudflare_turnstile'
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 5.0'
gem 'rollbar'
Expand Down
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ GEM
railties (>= 5.0.0)
faker (3.3.1)
i18n (>= 1.8.11, < 2)
faraday (2.10.0)
faraday-net_http (>= 2.0, < 3.2)
logger
faraday-net_http (3.1.0)
net-http
ferrum (0.14)
addressable (~> 2.5)
concurrent-ruby (~> 1.1)
Expand Down Expand Up @@ -183,6 +188,7 @@ GEM
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.0)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
Expand All @@ -202,6 +208,8 @@ GEM
ruby2_keywords (>= 0.0.5)
msgpack (1.7.2)
mutex_m (0.2.0)
net-http (0.4.1)
uri
net-imap (0.4.10)
date
net-protocol
Expand Down Expand Up @@ -272,6 +280,9 @@ GEM
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
rails_cloudflare_turnstile (0.2.1)
faraday (>= 1.0, < 3.0)
rails (>= 6.0, < 8)
railties (7.1.3.1)
actionpack (= 7.1.3.1)
activesupport (= 7.1.3.1)
Expand Down Expand Up @@ -360,6 +371,7 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
uri (0.13.0)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
Expand Down Expand Up @@ -407,6 +419,7 @@ DEPENDENCIES
rack-maintenance
rails (~> 7.1)
rails_autolink (~> 1.1)
rails_cloudflare_turnstile
redcarpet (~> 3.6)
redis (~> 5.0)
rollbar
Expand Down
1 change: 1 addition & 0 deletions app/controllers/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class RegistrationsController < ApplicationController
skip_before_action :authenticate
before_action :validate_cloudflare_turnstile, only: :create

def new
skip_authorization
Expand Down
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<% if Rails.env.production? %>
<script defer data-domain="jam.coop" src="https://plausible.io/js/script.js"></script>
<% end %>
<%= yield :head %>
</head>

<body class="bg-slate-100">
Expand Down
9 changes: 7 additions & 2 deletions app/views/registrations/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<% content_for :head do %>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer data-turbo-track="reload" data-turbo-temporary="true"></script>
<% end %>

<%= render 'shared/page_header', text: 'Sign up' %>

<%= form_with(url: sign_up_path, builder: TailwindFormBuilder) do |form| %>
<%= form_with(url: sign_up_path, builder: TailwindFormBuilder, data: { turbo: false }) do |form| %>
<%= render('shared/errors', model: @user) %>
<%= form.email_field :email, value: @user.email, required: true, autofocus: true, autocomplete: "email", class: 'w-full mb-3' %>
<%= form.password_field :password, required: true, autocomplete: "new-password", class: 'w-full mb-3' %>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", class: 'w-full mb-3' %>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", class: 'w-full mb-6' %>
<%= cloudflare_turnstile %>
<%= form.submit "Sign up", class: 'w-full mt-6' %>
<% end %>
8 changes: 8 additions & 0 deletions config/initializers/cloudflare_turnstile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

RailsCloudflareTurnstile.configure do |c|
c.site_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY')
c.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY')
c.fail_open = true
c.mock_enabled = false
end
11 changes: 11 additions & 0 deletions test/application_system_test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase

driven_by :cuprite

def setup
stub_successful_cloudflare_turnstile_request
end

def log_in_as(user)
visit log_in_url
fill_in :email, with: user.email
Expand All @@ -26,4 +30,11 @@ def sign_out
click_on 'avatar'
click_on 'Log out'
end

private

def stub_successful_cloudflare_turnstile_request
stub_request(:post, 'https://challenges.cloudflare.com/turnstile/v0/siteverify')
.to_return(status: 200, body: { success: true }.to_json)
end
end
13 changes: 13 additions & 0 deletions test/controllers/registrations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

class RegistrationsControllerTest < ActionDispatch::IntegrationTest
def setup
stub_request(:post, 'https://challenges.cloudflare.com/turnstile/v0/siteverify')
.to_return(status: 200, body: { success: true }.to_json)

@album = create(:album)
log_in_as(create(:user, admin: true))
end
Expand Down Expand Up @@ -58,4 +61,14 @@ def setup
assert_response :unprocessable_entity
assert_select 'h2', text: /errors prohibited this user from being saved/
end

test '#create makes a call to the cloudflare turnstile API to validate the request' do
post sign_up_url, params: {
email: '[email protected]',
password: 'Secret1*3*5*',
password_confirmation: 'Secret1*3*5*'
}

assert_requested :post, 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
end
end

0 comments on commit 009ce06

Please sign in to comment.