Devise Cloudflare Turnstile

Gem Version Ruby Rails Test Matrix Lint License

Automatically protect all Devise authentication forms with Cloudflare Turnstile. Add two helper tags to your layout, set your API keys, and every Devise form is protected — no changes to your Devise views or controllers.

Built on cloudflare-turnstile-rails for verification, widget rendering, CSP nonces, and Turbo support.

Supports Rails 5.0 → latest and Ruby 2.6 → latest, with the full Rails/Ruby matrix tested daily in CI.

"Buy Me A Coffee"

Features

  • No Devise view changes: Auto-injects the Turnstile widget into Devise forms via JavaScript.
  • Automatic verification: Checks Turnstile on every Devise create action.
  • Configurable: Skip protection per controller or action, and set widget appearance globally.
  • Plays well with your stack: Devise extensions (e.g. devise-invitable), Turbo/Turbolinks re-renders, and CSP nonces all work out of the box.
  • Built on cloudflare-turnstile-rails: verification, script loading, and i18n error messages.

Table of Contents

Getting Started

Installation

Add the gem to your Gemfile:

gem 'devise-cloudflare-turnstile'

Run bundle and the installer, which creates config/initializers/cloudflare_turnstile.rb:

bundle install
bin/rails generate devise_cloudflare_turnstile:install

Add these two helpers to your layout's <head> (e.g. app/views/layouts/application.html.erb):

<%= devise_turnstile_meta_tag %>
<%= devise_turnstile_scripts %>

Both render nothing on non-Devise pages, so they are safe to keep in a shared layout.

Set your Cloudflare Turnstile credentials via environment variables:

export CLOUDFLARE_TURNSTILE_SITE_KEY=your_site_key
export CLOUDFLARE_TURNSTILE_SECRET_KEY=your_secret_key

That's it — all Devise forms are now protected.

Getting API Keys

  1. Go to Cloudflare Turnstile Dashboard
  2. Create a new site
  3. Copy your Site Key and Secret Key

How It Works

  • Controller — a before_action on every Devise controller verifies the Turnstile response on create and re-renders the form with an error if it fails.
  • View — on Devise form pages, the two layout helpers emit the site key and load the injector JavaScript, which adds a cf-turnstile widget before each submit button.

Widget rendering, script loading, CSP nonces, and Turbo/Turbolinks handling are delegated to cloudflare-turnstile-rails.

Skipping Protection

Both the widget and server-side verification are applied per Devise action. You can disable them globally from the initializer or per controller.

Via configuration

Skip whole controllers or specific actions without creating custom controllers:

# config/initializers/cloudflare_turnstile.rb
Cloudflare::Turnstile::Rails.configure do |config|
  # ...site_key, secret_key, etc...

  config.skip :confirmations            # every action
  config.skip passwords: :create        # a single action
  config.skip unlocks: %i[new create]   # a set of actions
end

Controller keys match Devise's controller_name (sessions, registrations, passwords, confirmations, unlocks, invitations).

Via controller macro

If you already subclass a Devise controller, use the skip_turnstile macro:

# app/controllers/users/passwords_controller.rb
module Users
  class PasswordsController < Devise::PasswordsController
    skip_turnstile # every action
    # skip_turnstile only: :create # a subset
    # skip_turnstile except: :new  # everything but a subset
  end
end

Then update your routes:

devise_for :users, controllers: { passwords: 'users/passwords' }

Skipping an action suppresses both the widget injection and the server-side check for that action.

Customizing the Widget

Widget appearance (theme, language, size, and any other Cloudflare data-* option) is configured once in the initializer via the foundational gem's config.default_data. These defaults are applied to every auto-injected Devise widget:

# config/initializers/cloudflare_turnstile.rb
Cloudflare::Turnstile::Rails.configure do |config|
  config.default_data = {
    theme: 'dark',
    language: -> { I18n.locale }
  }
end

To use invisible or managed widgets everywhere, configure the matching widget type on your Cloudflare site key — that choice is made in the Cloudflare dashboard, not in code.

For a single form that needs different options, render a widget yourself in a custom Devise view using cloudflare_turnstile_tag. The injector skips any form that already contains a .cf-turnstile element, so your manual widget wins:

<%# app/views/users/sessions/new.html.erb %>
<%= cloudflare_turnstile_tag data: { theme: 'light' } %>

Passing Extra Verification Parameters

To forward additional siteverify parameters (e.g. remoteip, idempotency_key) to Cloudflare, override turnstile_verify_options in a custom Devise controller:

module Users
  class SessionsController < Devise::SessionsController
    private

    def turnstile_verify_options
      { remoteip: request.remote_ip }
    end
  end
end

How Protection Is Applied

Situation Behavior
No custom controllers All Devise controllers are protected automatically
Custom controller with skip_turnstile (or config skip) That controller/action opts out of both widget and verification
No custom views Devise's default views render; the widget still injects
Custom view without a widget The widget is still injected before the submit button
Custom view with cloudflare_turnstile_tag Your widget is used as-is; nothing is auto-injected
Invisible vs visible widget Determined by the site key's widget type in the Cloudflare dashboard

Automated Testing of Your Integration

Cloudflare provides dummy sitekeys and secret keys for development and testing. See the cloudflare-turnstile-rails testing docs for the full matrix.

Key Type Value Behavior
Site Key 1x00000000000000000000AA Always passes (visible)
Secret Key 1x0000000000000000000000000000000AA Always passes
Site Key 2x00000000000000000000AB Always blocks (visible)
Secret Key 2x0000000000000000000000000000000AA Always fails

Development

Setup

Install Ruby and JavaScript dependencies in one step:

bin/setup
npm install

Running the Test Suite

Appraisal is used to run the test suite against multiple Rails versions:

bundle exec appraisal install
bundle exec appraisal rake test

CI Note: The GitHub Action .github/workflows/test.yml runs this command across the supported Ruby matrix daily.

Code Linting

Run RuboCop and ESLint together:

bin/lint

CI Note: We run this via .github/workflows/lint.yml on the latest Ruby only.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/vkononov/devise-cloudflare-turnstile.

License

The gem is available as open source under the terms of the MIT License.