Devise Cloudflare Turnstile
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.
Features
- No Devise view changes: Auto-injects the Turnstile widget into Devise forms via JavaScript.
- Automatic verification: Checks Turnstile on every Devise
createaction. - 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
- Skipping Protection
- Customizing the Widget
- Passing Extra Verification Parameters
- How Protection Is Applied
- Automated Testing of Your Integration
- Development
- Contributing
- License
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
- Go to Cloudflare Turnstile Dashboard
- Create a new site
- Copy your Site Key and Secret Key
How It Works
- Controller — a
before_actionon every Devise controller verifies the Turnstile response oncreateand 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-turnstilewidget 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
{ 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.
