recaptcha 5.7.0 → 5.9.0

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: ec76ff47d12ef3388c2a45d56850272fff3675c9f59619a09b55c918f70b5494
4
- data.tar.gz: 697f0a4d41ffab63c6eb5b7d7b81c1f5ac2b1305f27bb84b5fccc421bcd38e2c
3
+ metadata.gz: b4e91df77500a77804749d34ac9690c4ddc9a8cfa30af5f2eae6b51c3d0a0e02
4
+ data.tar.gz: 2fa166f38a4e39ee6b2244bdfc91991b3bfc6d65360c6dd2e846d584352e3aab
5
5
  SHA512:
6
- metadata.gz: 51b3627583241aef9a99e2a3ed9c78091c87a92e87210749d38e9ccf0d418dbec455fd524d83210b8331be7064efac2be2308366b8288c574c53b84f6013333e
7
- data.tar.gz: 8018c4668d7eef292e5908d609a7ebdd5cfbfb84b0c86a361dac36d3982cc075dc7e694c51639dd09eaf148cfd9536cecb12f6fa188ec72f87328e85976a4e22
6
+ metadata.gz: acfb0b7bf9d211b2571dfcd8ee5a8195dc149effee4a04fe609d116f869722e583f4a5737e7d21174f2ff3d0f08c1f73f845dd4859daa107500e272230562052
7
+ data.tar.gz: 8c64efbfd826ab4a817fdbd37ef8e199d6a7c661b0efc18f6e4c5b26aca45a4412c8c0b331772561fe478d1f88c88b1a0cc655471c3eb751883a767a852680cf
data/CHANGELOG.md CHANGED
@@ -1,4 +1,9 @@
1
1
  ## Next
2
+ * Gracefully handle invalid params
3
+ * allow configuring response limit
4
+
5
+ ## 5.8.0
6
+ * Add support for the enterprise API
2
7
 
3
8
  ## 5.7.0
4
9
  * french locale
data/README.md CHANGED
@@ -68,6 +68,14 @@ export RECAPTCHA_SITE_KEY = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
68
68
  export RECAPTCHA_SECRET_KEY = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
69
69
  ```
70
70
 
71
+ If you have an Enterprise API key:
72
+
73
+ ```shell
74
+ export RECAPTCHA_ENTERPRISE = 'true'
75
+ export RECAPTCHA_ENTERPRISE_API_KEY = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA'
76
+ export RECAPTCHA_ENTERPRISE_PROJECT_ID = 'my-project'
77
+ ```
78
+
71
79
  Add `recaptcha_tags` to the forms you want to protect:
72
80
 
73
81
  ```erb
@@ -159,16 +167,18 @@ you like.
159
167
 
160
168
  Some of the options available:
161
169
 
162
- | Option | Description |
163
- |----------------|-------------|
164
- | `:model` | Model to set errors.
165
- | `:attribute` | Model attribute to receive errors. (default: `:base`)
166
- | `:message` | Custom error message.
167
- | `:secret_key` | Override the secret API key from the configuration.
168
- | `:timeout` | The number of seconds to wait for reCAPTCHA servers before give up. (default: `3`)
169
- | `:response` | Custom response parameter. (default: `params['g-recaptcha-response-data']`)
170
- | `:hostname` | Expected hostname or a callable that validates the hostname, see [domain validation](https://developers.google.com/recaptcha/docs/domain_validation) and [hostname](https://developers.google.com/recaptcha/docs/verify#api-response) docs. (default: `nil`, but can be changed by setting `config.hostname`)
171
- | `:env` | Current environment. The request to verify will be skipped if the environment is specified in configuration under `skip_verify_env`
170
+ | Option | Description |
171
+ |---------------------------|-------------|
172
+ | `:model` | Model to set errors.
173
+ | `:attribute` | Model attribute to receive errors. (default: `:base`)
174
+ | `:message` | Custom error message.
175
+ | `:secret_key` | Override the secret API key from the configuration.
176
+ | `:enterprise_api_key` | Override the Enterprise API key from the configuration.
177
+ | `:enterprise_project_id ` | Override the Enterprise project ID from the configuration.
178
+ | `:timeout` | The number of seconds to wait for reCAPTCHA servers before give up. (default: `3`)
179
+ | `:response` | Custom response parameter. (default: `params['g-recaptcha-response-data']`)
180
+ | `:hostname` | Expected hostname or a callable that validates the hostname, see [domain validation](https://developers.google.com/recaptcha/docs/domain_validation) and [hostname](https://developers.google.com/recaptcha/docs/verify#api-response) docs. (default: `nil`, but can be changed by setting `config.hostname`)
181
+ | `:env` | Current environment. The request to verify will be skipped if the environment is specified in configuration under `skip_verify_env`
172
182
 
173
183
 
174
184
  ### `invisible_recaptcha_tags`
@@ -376,17 +386,19 @@ then you can either:
376
386
  2. write and specify a custom `callback` function. You may also want to pass `element: false` if you
377
387
  don't have a use for the hidden input element.
378
388
 
379
- Note that you cannot submit/verify the same response token more than once or you will get a
380
- `timeout-or-duplicate` error code. If you need reset the captcha and generate a new response token,
381
- then you need to call `grecaptcha.execute(…)` again. This helper provides a JavaScript method (for
382
- each action) named `executeRecaptchaFor{action}` to make this easier. That is the same method that
383
- is invoked immediately. It simply calls `grecaptcha.execute` again and then calls the `callback`
384
- function with the response token.
389
+ Note that you cannot submit/verify the same response token more than once or you
390
+ will get a `timeout-or-duplicate` error code. If you need reset the captcha and
391
+ generate a new response token, then you need to call `grecaptcha.execute(…)` or
392
+ `grecaptcha.enterprise.execute(…)` again. This helper provides a JavaScript
393
+ method (for each action) named `executeRecaptchaFor{action}` to make this
394
+ easier. That is the same method that is invoked immediately. It simply calls
395
+ `grecaptcha.execute` or `grecaptcha.enterprise.execute` again and then calls the
396
+ `callback` function with the response token.
385
397
 
386
398
  You will also get a `timeout-or-duplicate` error if too much time has passed between getting the
387
399
  response token and verifying it. This can easily happen with large forms that take the user a couple
388
400
  minutes to complete. Unlike v2, where you can use the `expired-callback` to be notified when the
389
- response expries, v3 appears to provide no such callback. See also
401
+ response expires, v3 appears to provide no such callback. See also
390
402
  [1](https://github.com/google/recaptcha/issues/281) and
391
403
  [2](https://stackoverflow.com/questions/54437745/recaptcha-v3-how-to-deal-with-expired-token-after-idle).
392
404
 
@@ -446,7 +458,7 @@ According to https://developers.google.com/recaptcha/docs/v3#placement,
446
458
 
447
459
  > Note: You can execute reCAPTCHA as many times as you'd like with different actions on the same page.
448
460
 
449
- You will need to verify each action individually with separate call to `verify_recaptcha`.
461
+ You will need to verify each action individually with a separate call to `verify_recaptcha`.
450
462
 
451
463
  ```ruby
452
464
  result_a = verify_recaptcha(action: 'a')
@@ -506,14 +518,20 @@ Recaptcha.configuration.skip_verify_env.delete("test")
506
518
  Recaptcha.configure do |config|
507
519
  config.site_key = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
508
520
  config.secret_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
521
+
509
522
  # Uncomment the following line if you are using a proxy server:
510
523
  # config.proxy = 'http://myproxy.com.au:8080'
524
+
525
+ # Uncomment the following lines if you are using the Enterprise API:
526
+ # config.enterprise = true
527
+ # config.enterprise_api_key = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA'
528
+ # config.enterprise_project_id = 'my-project'
511
529
  end
512
530
  ```
513
531
 
514
532
  ### Recaptcha.with_configuration
515
533
 
516
- For temporary overwrites (not thread safe).
534
+ For temporary overwrites (not thread-safe).
517
535
 
518
536
  ```ruby
519
537
  Recaptcha.with_configuration(site_key: '12345') do
@@ -533,6 +551,28 @@ recaptcha_tags site_key: '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
533
551
  verify_recaptcha secret_key: '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
534
552
  ```
535
553
 
554
+
555
+ ## hCaptcha support
556
+
557
+ [hCaptcha](https://hcaptcha.com) is an alternative service providing reCAPTCHA API.
558
+
559
+ To use hCaptcha:
560
+ 1. Set a site and a secret key as usual
561
+ 2. Set two options in `verify_url` and `api_service_url` pointing to hCaptcha API endpoints.
562
+ 3. Disable a response limit check by setting a `response_limit` to the negative or large enough value (reCAPTCHA is limited by 4000 characters).
563
+ 4. It is not required to change a parameter name as [official docs suggest](https://docs.hcaptcha.com/switch) because API handles standard `g-recaptcha` for compatibility.
564
+
565
+ ```ruby
566
+ # config/initializers/recaptcha.rb
567
+ Recaptcha.configure do |config|
568
+ config.site_key = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
569
+ config.secret_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'
570
+ config.verify_url = 'https://hcaptcha.com/siteverify'
571
+ config.api_server_url = 'https://hcaptcha.com/1/api.js'
572
+ config.response_limit = -1
573
+ end
574
+ ```
575
+
536
576
  ## Misc
537
577
  - Check out the [wiki](https://github.com/ambethia/recaptcha/wiki) and leave whatever you found valuable there.
538
578
  - [Add multiple widgets to the same page](https://github.com/ambethia/recaptcha/wiki/Add-multiple-widgets-to-the-same-page)
@@ -83,10 +83,12 @@ module Recaptcha
83
83
  # @return [String] A response token if one was passed in the params; otherwise, `''`
84
84
  def recaptcha_response_token(action = nil)
85
85
  response_param = params['g-recaptcha-response-data'] || params['g-recaptcha-response']
86
- if response_param&.respond_to?(:to_h) # Includes ActionController::Parameters
87
- response_param[action].to_s
86
+ response_param = response_param[action] if action && response_param.respond_to?(:key?)
87
+
88
+ if response_param.is_a?(String)
89
+ response_param
88
90
  else
89
- response_param.to_s
91
+ ''
90
92
  end
91
93
  end
92
94
  end
@@ -31,11 +31,14 @@ module Recaptcha
31
31
  #
32
32
  class Configuration
33
33
  DEFAULTS = {
34
- 'server_url' => 'https://www.recaptcha.net/recaptcha/api.js',
35
- 'verify_url' => 'https://www.recaptcha.net/recaptcha/api/siteverify'
34
+ 'free_server_url' => 'https://www.recaptcha.net/recaptcha/api.js',
35
+ 'enterprise_server_url' => 'https://www.recaptcha.net/recaptcha/enterprise.js',
36
+ 'free_verify_url' => 'https://www.recaptcha.net/recaptcha/api/siteverify',
37
+ 'enterprise_verify_url' => 'https://recaptchaenterprise.googleapis.com/v1beta1/projects'
36
38
  }.freeze
37
39
 
38
- attr_accessor :default_env, :skip_verify_env, :secret_key, :site_key, :proxy, :handle_timeouts_gracefully, :hostname
40
+ attr_accessor :default_env, :skip_verify_env, :proxy, :secret_key, :site_key, :handle_timeouts_gracefully, :hostname
41
+ attr_accessor :enterprise, :enterprise_api_key, :enterprise_project_id, :response_limit
39
42
  attr_writer :api_server_url, :verify_url
40
43
 
41
44
  def initialize #:nodoc:
@@ -45,8 +48,15 @@ module Recaptcha
45
48
 
46
49
  @secret_key = ENV['RECAPTCHA_SECRET_KEY']
47
50
  @site_key = ENV['RECAPTCHA_SITE_KEY']
51
+
52
+ @enterprise = ENV['RECAPTCHA_ENTERPRISE'] == 'true'
53
+ @enterprise_api_key = ENV['RECAPTCHA_ENTERPRISE_API_KEY']
54
+ @enterprise_project_id = ENV['RECAPTCHA_ENTERPRISE_PROJECT_ID']
55
+
48
56
  @verify_url = nil
49
57
  @api_server_url = nil
58
+
59
+ @response_limit = 4000
50
60
  end
51
61
 
52
62
  def secret_key!
@@ -57,12 +67,20 @@ module Recaptcha
57
67
  site_key || raise(RecaptchaError, "No site key specified.")
58
68
  end
59
69
 
70
+ def enterprise_api_key!
71
+ enterprise_api_key || raise(RecaptchaError, "No Enterprise API key specified.")
72
+ end
73
+
74
+ def enterprise_project_id!
75
+ enterprise_project_id || raise(RecaptchaError, "No Enterprise project ID specified.")
76
+ end
77
+
60
78
  def api_server_url
61
- @api_server_url || DEFAULTS.fetch('server_url')
79
+ @api_server_url || (enterprise ? DEFAULTS.fetch('enterprise_server_url') : DEFAULTS.fetch('free_server_url'))
62
80
  end
63
81
 
64
82
  def verify_url
65
- @verify_url || DEFAULTS.fetch('verify_url')
83
+ @verify_url || (enterprise ? DEFAULTS.fetch('enterprise_verify_url') : DEFAULTS.fetch('free_verify_url'))
66
84
  end
67
85
  end
68
86
  end
@@ -174,7 +174,8 @@ module Recaptcha
174
174
 
175
175
  # v3
176
176
 
177
- # Renders a script that calls `grecaptcha.execute` for the given `site_key` and `action` and
177
+ # Renders a script that calls `grecaptcha.execute` or
178
+ # `grecaptcha.enterprise.execute` for the given `site_key` and `action` and
178
179
  # calls the `callback` with the resulting response token.
179
180
  private_class_method def self.recaptcha_v3_inline_script(site_key, action, callback, id, options = {})
180
181
  nonce = options[:nonce]
@@ -185,8 +186,8 @@ module Recaptcha
185
186
  // Define function so that we can call it again later if we need to reset it
186
187
  // This executes reCAPTCHA and then calls our callback.
187
188
  function #{recaptcha_v3_execute_function_name(action)}() {
188
- grecaptcha.ready(function() {
189
- grecaptcha.execute('#{site_key}', {action: '#{action}'}).then(function(token) {
189
+ #{recaptcha_ready_method_name}(function() {
190
+ #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}).then(function(token) {
190
191
  #{callback}('#{id}', token)
191
192
  });
192
193
  });
@@ -199,8 +200,8 @@ module Recaptcha
199
200
  // Returns a Promise that resolves with the response token.
200
201
  async function #{recaptcha_v3_async_execute_function_name(action)}() {
201
202
  return new Promise((resolve, reject) => {
202
- grecaptcha.ready(async function() {
203
- resolve(await grecaptcha.execute('#{site_key}', {action: '#{action}'}))
203
+ #{recaptcha_ready_method_name}(async function() {
204
+ resolve(await #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}))
204
205
  });
205
206
  })
206
207
  };
@@ -217,8 +218,8 @@ module Recaptcha
217
218
  <<-HTML
218
219
  <script#{nonce_attr}>
219
220
  function #{recaptcha_v3_execute_function_name(action)}() {
220
- grecaptcha.ready(function() {
221
- grecaptcha.execute('#{site_key}', {action: '#{action}'}).then(function(token) {
221
+ #{recaptcha_ready_method_name}(function() {
222
+ #{recaptcha_execute_method_name}('#{site_key}', {action: '#{action}'}).then(function(token) {
222
223
  #{callback}('#{id}', token)
223
224
  });
224
225
  });
@@ -251,8 +252,9 @@ module Recaptcha
251
252
  recaptcha_v3_inline_script?(options)
252
253
  end
253
254
 
254
- # Returns the name of the JavaScript function that actually executes the reCAPTCHA code (calls
255
- # grecaptcha.execute). You can call it again later to reset it.
255
+ # Returns the name of the JavaScript function that actually executes the
256
+ # reCAPTCHA code (calls `grecaptcha.execute` or
257
+ # `grecaptcha.enterprise.execute`). You can call it again later to reset it.
256
258
  def self.recaptcha_v3_execute_function_name(action)
257
259
  "executeRecaptchaFor#{sanitize_action_for_js(action)}"
258
260
  end
@@ -296,6 +298,14 @@ module Recaptcha
296
298
  HTML
297
299
  end
298
300
 
301
+ def self.recaptcha_execute_method_name
302
+ Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.execute" : "grecaptcha.execute"
303
+ end
304
+
305
+ def self.recaptcha_ready_method_name
306
+ Recaptcha.configuration.enterprise ? "grecaptcha.enterprise.ready" : "grecaptcha.ready"
307
+ end
308
+
299
309
  private_class_method def self.default_callback_required?(options)
300
310
  options[:callback] == 'invisibleRecaptchaSubmit' &&
301
311
  !Recaptcha.skip_env?(options[:env]) &&
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Recaptcha
4
- VERSION = '5.7.0'
4
+ VERSION = '5.9.0'
5
5
  end
data/lib/recaptcha.rb CHANGED
@@ -14,7 +14,6 @@ end
14
14
 
15
15
  module Recaptcha
16
16
  DEFAULT_TIMEOUT = 3
17
- RESPONSE_LIMIT = 4000
18
17
 
19
18
  class RecaptchaError < StandardError
20
19
  end
@@ -56,15 +55,48 @@ module Recaptcha
56
55
  end
57
56
 
58
57
  def self.invalid_response?(resp)
59
- resp.empty? || resp.length > RESPONSE_LIMIT
58
+ resp.empty? || resp.length > configuration.response_limit
60
59
  end
61
60
 
62
61
  def self.verify_via_api_call(response, options)
62
+ if Recaptcha.configuration.enterprise
63
+ verify_via_api_call_enterprise(response, options)
64
+ else
65
+ verify_via_api_call_free(response, options)
66
+ end
67
+ end
68
+
69
+ def self.verify_via_api_call_enterprise(response, options)
70
+ site_key = options.fetch(:site_key) { configuration.site_key! }
71
+ api_key = options.fetch(:enterprise_api_key) { configuration.enterprise_api_key! }
72
+ project_id = options.fetch(:enterprise_project_id) { configuration.enterprise_project_id! }
73
+
74
+ query_params = { 'key' => api_key }
75
+ body = { 'event' => { 'token' => response, 'siteKey' => site_key } }
76
+ body['event']['expectedAction'] = options[:action] if options.key?(:action)
77
+ body['event']['userIpAddress'] = options[:remote_ip] if options.key?(:remote_ip)
78
+
79
+ reply = api_verification_enterprise(query_params, body, project_id, timeout: options[:timeout])
80
+ token_properties = reply['tokenProperties']
81
+ success = !token_properties.nil? &&
82
+ token_properties['valid'].to_s == 'true' &&
83
+ hostname_valid?(token_properties['hostname'], options[:hostname]) &&
84
+ action_valid?(token_properties['action'], options[:action]) &&
85
+ score_above_threshold?(reply['score'], options[:minimum_score])
86
+
87
+ if options[:with_reply] == true
88
+ return success, reply
89
+ else
90
+ return success
91
+ end
92
+ end
93
+
94
+ def self.verify_via_api_call_free(response, options)
63
95
  secret_key = options.fetch(:secret_key) { configuration.secret_key! }
64
96
  verify_hash = { 'secret' => secret_key, 'response' => response }
65
97
  verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip)
66
98
 
67
- reply = api_verification(verify_hash, timeout: options[:timeout])
99
+ reply = api_verification_free(verify_hash, timeout: options[:timeout])
68
100
  success = reply['success'].to_s == 'true' &&
69
101
  hostname_valid?(reply['hostname'], options[:hostname]) &&
70
102
  action_valid?(reply['action'], options[:action]) &&
@@ -105,7 +137,7 @@ module Recaptcha
105
137
  end
106
138
  end
107
139
 
108
- def self.api_verification(verify_hash, timeout: nil)
140
+ def self.http_client_for(uri:, timeout: nil)
109
141
  timeout ||= DEFAULT_TIMEOUT
110
142
  http = if configuration.proxy
111
143
  proxy_server = URI.parse(configuration.proxy)
@@ -113,12 +145,28 @@ module Recaptcha
113
145
  else
114
146
  Net::HTTP
115
147
  end
148
+ instance = http.new(uri.host, uri.port)
149
+ instance.read_timeout = instance.open_timeout = timeout
150
+ instance.use_ssl = true if uri.port == 443
151
+
152
+ instance
153
+ end
154
+
155
+ def self.api_verification_free(verify_hash, timeout: nil)
116
156
  query = URI.encode_www_form(verify_hash)
117
157
  uri = URI.parse(configuration.verify_url + '?' + query)
118
- http_instance = http.new(uri.host, uri.port)
119
- http_instance.read_timeout = http_instance.open_timeout = timeout
120
- http_instance.use_ssl = true if uri.port == 443
158
+ http_instance = http_client_for(uri: uri, timeout: timeout)
121
159
  request = Net::HTTP::Get.new(uri.request_uri)
122
160
  JSON.parse(http_instance.request(request).body)
123
161
  end
162
+
163
+ def self.api_verification_enterprise(query_params, body, project_id, timeout: nil)
164
+ query = URI.encode_www_form(query_params)
165
+ uri = URI.parse(configuration.verify_url + "/#{project_id}/assessments" + '?' + query)
166
+ http_instance = http_client_for(uri: uri, timeout: timeout)
167
+ request = Net::HTTP::Post.new(uri.request_uri)
168
+ request['Content-Type'] = 'application/json; charset=utf-8'
169
+ request.body = JSON.generate(body)
170
+ JSON.parse(http_instance.request(request).body)
171
+ end
124
172
  end
data/rails/locales/fr.yml CHANGED
@@ -2,4 +2,4 @@ fr:
2
2
  recaptcha:
3
3
  errors:
4
4
  verification_failed: La vérification reCAPTCHA a échoué, veuillez essayer à nouveau.
5
- recaptcha_unreachable: Oops, nous n'avons pas vu valider votre réponse reCAPTCHA. Veuillez essayer à nouveau.
5
+ recaptcha_unreachable: Oops, nous n'avons pas pu valider votre réponse reCAPTCHA. Veuillez essayer à nouveau.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recaptcha
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.7.0
4
+ version: 5.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason L Perry
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-05 00:00:00.000000000 Z
11
+ date: 2022-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -176,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
176
  - !ruby/object:Gem::Version
177
177
  version: '0'
178
178
  requirements: []
179
- rubygems_version: 3.1.3
179
+ rubygems_version: 3.1.6
180
180
  signing_key:
181
181
  specification_version: 4
182
182
  summary: Helpers for the reCAPTCHA API