devise-cloudflare-turnstile 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95edf74912abe68de902d9a5e3cf58c59f8f44d3a60f0b5cfb3ac6105e981ff6
4
- data.tar.gz: b01582121ba114a8044e010fcd782e8baca8100a220150c4eb6dc2f85af9ac7d
3
+ metadata.gz: 471aea45843200703b848a8aacef060af2362f7103fae9f417df4f9e092bae12
4
+ data.tar.gz: a33a85702790274610f4580978a446037f5c468680a0128517105d14f58d9061
5
5
  SHA512:
6
- metadata.gz: dc8dba667dcec474d7464faa259cccc99abaa45823fcedfe26520098ede8b9fd4c78a0849cf0c0e03b85550e0f7cd2527a55c7d60628ba0f243f17aa7074e0ee
7
- data.tar.gz: 05ccb44987629b192af3087fcbe463989b30791f31526c3f8c134dc49e45cf5d6e79b7d44dba4de5d948b3ddc23ba55fa6da1ef18a56cfbfdcab9113691860cf
6
+ metadata.gz: b4ada2a98896212e2b0d0c60ac5f81cbf0c56cd204c026273617eefec30aece67b114f6809d2cf81261876c6ac77c4348972dbd0abd48f3e3c7d7794697bc9da
7
+ data.tar.gz: e57e4598eec7ea12bfbad15dfe25855322e0b9d48cef81114b49ef5b66115c6e063f8c7b9bc28a68a5e29390a4c54c58aaf3f7eabc5eeebc000170d059ee55a9
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Rails](https://img.shields.io/badge/Rails-5.0%20to%208.1-D30001?logo=rubyonrails&logoColor=white)](https://github.com/vkononov/devise-cloudflare-turnstile/blob/main/Appraisals)
6
6
  [![Test Matrix](https://img.shields.io/github/actions/workflow/status/vkononov/devise-cloudflare-turnstile/test.yml?branch=main&label=Test%20Matrix&logo=github)](https://github.com/vkononov/devise-cloudflare-turnstile/actions/workflows/test.yml)
7
7
  [![Lint](https://img.shields.io/github/actions/workflow/status/vkononov/devise-cloudflare-turnstile/lint.yml?branch=main&label=Lint&logo=github)](https://github.com/vkononov/devise-cloudflare-turnstile/actions/workflows/lint.yml)
8
- [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/MIT)
9
9
 
10
10
  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.
11
11
 
@@ -84,8 +84,8 @@ That's it — all Devise forms are now protected.
84
84
 
85
85
  ### How It Works
86
86
 
87
- * **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.
88
- * **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. Rails `button_to` action buttons (e.g. OAuth sign-in or log out) are skipped.
87
+ * **Controller** — a `before_action` on every Devise controller verifies the Turnstile response on `create` and re-renders the form if it fails, reporting the failure as `flash.now[:alert]`. Your layout must render `alert` for the message to be seen.
88
+ * **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. Only forms with a field a person fills in are considered, so action-only POST forms such as OAuth sign-in and log-out buttons are skipped.
89
89
 
90
90
  The widget appears only on the `create` forms (and their `new` pages) — sign in, sign up, password-reset request, and resend confirmation/unlock. Authenticated and edit pages such as account settings, password-reset completion, and accepting an invitation are left untouched.
91
91
 
@@ -182,7 +182,8 @@ end
182
182
  | Edit / authenticated pages (account settings, password-reset completion, accept invitation) | Not protected — the widget only appears on the `create` forms |
183
183
  | Custom controller with `skip_turnstile` (or config `skip`) | That controller/action opts out of both widget and verification |
184
184
  | No custom views | Devise's default views render; the widget still injects |
185
- | Custom view without a widget | The widget is injected before the submit button; Rails `button_to` action buttons are skipped |
185
+ | Custom view without a widget | The widget is injected before the submit button if the form has a fillable field: a text, email, password, `textarea` or `select` field counts, a hidden input, checkbox, radio or file input does not |
186
+ | Action-only POST form (OAuth sign-in, log out, delete) | No fillable field, so no widget |
186
187
  | Custom view with `cloudflare_turnstile_tag` | Your widget is used as-is; nothing is auto-injected |
187
188
  | Invisible vs visible widget | Determined by the site key's widget type in the Cloudflare dashboard |
188
189
 
@@ -194,7 +195,7 @@ Cloudflare provides dummy sitekeys and secret keys for development and testing.
194
195
  |----------|-------|----------|
195
196
  | Site Key | `1x00000000000000000000AA` | Always passes (visible) |
196
197
  | Secret Key | `1x0000000000000000000000000000000AA` | Always passes |
197
- | Site Key | `2x00000000000000000000AB` | Always blocks (visible) |
198
+ | Site Key | `2x00000000000000000000AB` | Always fails (visible) |
198
199
  | Secret Key | `2x0000000000000000000000000000000AA` | Always fails |
199
200
 
200
201
  ## Development
@@ -227,7 +228,7 @@ Run RuboCop and ESLint together:
227
228
  bin/lint
228
229
  ```
229
230
 
230
- > **CI Note:** We run this via [.github/workflows/lint.yml](.github/workflows/lint.yml) on the latest Ruby only.
231
+ > **CI Note:** We run `bin/lint --no-fix` via [.github/workflows/lint.yml](.github/workflows/lint.yml) on the latest Ruby only.
231
232
 
232
233
  ## Contributing
233
234
 
@@ -43,6 +43,15 @@
43
43
  }
44
44
  }
45
45
 
46
+ // Hidden inputs, checkboxes, radios and file inputs don't count as fillable.
47
+ function hasFillableField(form) {
48
+ var selector = 'input[type="text"], input[type="email"], input[type="password"], ' +
49
+ 'input[type="search"], input[type="tel"], input[type="url"], ' +
50
+ 'input[type="number"], input:not([type]), textarea, select';
51
+
52
+ return form.querySelector(selector) !== null;
53
+ }
54
+
46
55
  function renderWidget(widget) {
47
56
  if (widget.dataset.initialized || widget.childElementCount > 0) {
48
57
  return;
@@ -74,18 +83,17 @@
74
83
  return;
75
84
  }
76
85
 
77
- /*
78
- * Skip Rails button_to forms (rendered with class "button_to"), such as
79
- * OAuth sign-in, log out, or delete buttons. They are action buttons, not
80
- * user-input forms, so they should never get a Turnstile widget.
81
- */
82
- forms = document.querySelectorAll('form[method="post"]:not(.button_to), form[method="POST"]:not(.button_to)');
86
+ forms = document.querySelectorAll('form[method="post"], form[method="POST"]');
83
87
 
84
88
  for (i = 0; i < forms.length; i++) {
85
89
  form = forms[i];
86
90
  widget = form.querySelector('.' + WIDGET_CLASS);
87
91
 
88
92
  if (!widget) {
93
+ if (!hasFillableField(form)) {
94
+ continue;
95
+ }
96
+
89
97
  submit = form.querySelector('input[type="submit"], button[type="submit"], button:not([type])');
90
98
 
91
99
  if (!submit) {
@@ -10,6 +10,7 @@ module Devise
10
10
  class_attribute :_turnstile_skip_rules, instance_accessor: false
11
11
  self._turnstile_skip_rules = []
12
12
 
13
+ prepend_before_action :prevalidate_cloudflare_turnstile!, if: :turnstile_verify_action?
13
14
  before_action :verify_cloudflare_turnstile!, if: :turnstile_verify_action?
14
15
  before_action :set_turnstile_page_marker, if: :turnstile_form_action?
15
16
  end
@@ -30,23 +31,52 @@ module Devise
30
31
 
31
32
  private
32
33
 
33
- def verify_cloudflare_turnstile! # rubocop:disable Metrics/AbcSize
34
+ # Runs ahead of the application's own filters, since any filter that
35
+ # reads the signed-in user lets Warden authenticate from the posted
36
+ # credentials. verify_cloudflare_turnstile! still renders the failure.
37
+ def prevalidate_cloudflare_turnstile!
38
+ return unless turnstile_devise_resource?
34
39
  return if turnstile_skipped?
40
+ return if turnstile_verified?
35
41
 
36
- self.resource ||= resource_class.new
37
- return if valid_turnstile?(model: resource, **turnstile_verify_options)
42
+ revoke_params_authentication!
43
+ end
38
44
 
39
- restore_turnstile_submitted_values
45
+ def verify_cloudflare_turnstile!
46
+ return if turnstile_skipped?
47
+ return if turnstile_verified?
40
48
 
41
- # Sessions (and some other Devise views) do not render resource errors.
42
- # Always surface the failure via flash so the user sees feedback.
43
- flash.now[:alert] = ::Cloudflare::Turnstile::Rails::ErrorMessage.default
49
+ revoke_params_authentication!
50
+ restore_turnstile_submitted_values
44
51
  clean_up_passwords(resource) if respond_to?(:clean_up_passwords, true)
45
52
  set_minimum_password_length if respond_to?(:set_minimum_password_length, true)
46
53
  set_turnstile_page_marker
47
54
  render turnstile_failure_action, status: :unprocessable_entity
48
55
  end
49
56
 
57
+ # Memoised because Cloudflare tokens are single use, so the early check
58
+ # and a direct verify_cloudflare_turnstile! call verify only once.
59
+ def turnstile_verified?
60
+ return @_turnstile_verified unless @_turnstile_verified.nil?
61
+
62
+ self.resource ||= resource_class.new
63
+ # No model is passed, so the failure stays out of resource.errors and is
64
+ # reported through the flash, scoped to the render that follows.
65
+ @_turnstile_verified = valid_turnstile?(flash: :now, **turnstile_verify_options)
66
+ end
67
+
68
+ # Devise asserts the mapping in its own prepended callback, which runs
69
+ # after ours. Standing down without one keeps its clearer error.
70
+ def turnstile_devise_resource?
71
+ !respond_to?(:devise_mapping, true) || !devise_mapping.nil?
72
+ end
73
+
74
+ # Devise's sessions#create marks the request eligible for authentication
75
+ # from the posted credentials, and publishes no constant for the key.
76
+ def revoke_params_authentication!
77
+ request.env.delete('devise.allow_params_authentication')
78
+ end
79
+
50
80
  # Re-populate the resource with the values the user just submitted (minus
51
81
  # passwords) so the re-rendered form keeps their input. We fail in a
52
82
  # before_action, before Devise builds the resource from params, so
@@ -1,7 +1,7 @@
1
1
  module Devise
2
2
  module Cloudflare
3
3
  module Turnstile
4
- VERSION = '0.11.0'.freeze
4
+ VERSION = '0.12.1'.freeze
5
5
  end
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-cloudflare-turnstile
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadim Kononov
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.1'
18
+ version: '1.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '1.1'
25
+ version: '1.2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: devise
28
28
  requirement: !ruby/object:Gem::Requirement