Skip to content

Commit

Permalink
Merge pull request #459 from holli/feature/expose_verification_issues
Browse files Browse the repository at this point in the history
Add recaptcha_failure_reason for enchanced error reporting
  • Loading branch information
grosser authored Dec 8, 2024
2 parents 04ad183 + 161c4c4 commit cc575f3
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Update to latest version of rubocop
* Drop support for Ruby 2.7; add Ruby 3.3
* Add i18n: de, es, it, pt, pt-BR
* Added recaptcha_failure_reason

## 5.16.0
* Allow usage of `options[:turbo]` as well as `options[:turbolinks]` for `recaptcha_v3`
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export RECAPTCHA_ENTERPRISE_API_KEY = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyup
export RECAPTCHA_ENTERPRISE_PROJECT_ID = 'my-project'
```

_note:_ you'll still have to provide `RECAPTCHA_SITE_KEY`, which will hold the value of your enterprise recaptcha key id. You will not need to provide a `RECAPTCHA_SECRET_KEY`, however.
_note:_ you'll still have to provide `RECAPTCHA_SITE_KEY`, which will hold the value of your enterprise recaptcha key id. You will not need to provide a `RECAPTCHA_SECRET_KEY`, however.

`RECAPTCHA_ENTERPRISE_API_KEY` is the enterprise key of your Google Cloud Project, which you can generate here: https://console.cloud.google.com/apis/credentials.
`RECAPTCHA_ENTERPRISE_API_KEY` is the enterprise key of your Google Cloud Project, which you can generate here: https://console.cloud.google.com/apis/credentials.

Add `recaptcha_tags` to the forms you want to protect:

Expand Down Expand Up @@ -488,7 +488,7 @@ are passed as a hash under `params['g-recaptcha-response-data']` with the action
It is recommended to pass `external_script: false` on all but one of the calls to
`recaptcha` since you only need to include the script tag once for a given `site_key`.

## `recaptcha_reply`
## `recaptcha_reply` and `recaptcha_failure_reason`

After `verify_recaptcha` has been called, you can call `recaptcha_reply` to get the raw reply from recaptcha. This can allow you to get the exact score returned by recaptcha should you need it.

Expand All @@ -504,6 +504,8 @@ end

`recaptcha_reply` will return `nil` if the the reply was not yet fetched.

`recaptcha_failure_reason` will return information if verification failed. E.g. if params was wrong or api resulted some error-codes.

## I18n support

reCAPTCHA supports the I18n gem (it comes with English translations)
Expand Down
23 changes: 22 additions & 1 deletion lib/recaptcha/adapters/controller_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def verify_recaptcha(options = {})

begin
verified = if Recaptcha.invalid_response?(recaptcha_response)
@_recaptcha_failure_reason = if recaptcha_response.nil?
"No recaptcha response/param(:action) found."
else
"Recaptcha response/param(:action) was invalid."
end
false
else
unless options[:skip_remote_ip]
Expand All @@ -26,10 +31,21 @@ def verify_recaptcha(options = {})

success, @_recaptcha_reply =
Recaptcha.verify_via_api_call(recaptcha_response, options.merge(with_reply: true))
unless success
@_recaptcha_failure_reason = if @_recaptcha_reply["score"] &&
@_recaptcha_reply["score"].to_f < options[:minimum_score].to_f
"Recaptcha score didn't exceed the minimum: #{@_recaptcha_reply["score"]} < #{options[:minimum_score]}."
elsif @_recaptcha_reply['error-codes']
"Recaptcha api call returned with error-codes: #{@_recaptcha_reply['error-codes']}."
else
"Recaptcha failure after api call. Api reply: #{@_recaptcha_reply}."
end
end
success
end

if verified
@_recaptcha_failure_reason = nil
flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
true
else
Expand All @@ -41,6 +57,7 @@ def verify_recaptcha(options = {})
false
end
rescue Timeout::Error
@_recaptcha_failure_reason = "Recaptcha server unreachable."
if Recaptcha.configuration.handle_timeouts_gracefully
recaptcha_error(
model,
Expand All @@ -57,13 +74,17 @@ def verify_recaptcha(options = {})
end

def verify_recaptcha!(options = {})
verify_recaptcha(options) || raise(VerifyError)
verify_recaptcha(options) || raise(VerifyError, @_recaptcha_failure_reason)
end

def recaptcha_reply
@_recaptcha_reply if defined?(@_recaptcha_reply)
end

def recaptcha_failure_reason
@_recaptcha_failure_reason
end

def recaptcha_error(model, attribute, message)
if model
model.errors.add(attribute, message)
Expand Down
33 changes: 33 additions & 0 deletions test/verify_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize
public :verify_recaptcha!
public :recaptcha_reply
public :recaptcha_response_token
public :recaptcha_failure_reason
end

describe 'controller helpers' do
Expand All @@ -36,6 +37,22 @@ def initialize

assert_equal :foo, @controller.verify_recaptcha!
end

it "raise with informative error message when it fails" do
response_hash = {
success: true,
action: 'homepage',
score: 0.4
}

expect_http_post.to_return(body: response_hash.to_json)

error = assert_raises Recaptcha::VerifyError do
@controller.verify_recaptcha!(minimum_score: 0.9)
end

assert_equal "Recaptcha score didn't exceed the minimum: 0.4 < 0.9.", error.message
end
end

describe "#verify_recaptcha" do
Expand All @@ -59,6 +76,7 @@ def initialize

refute @controller.verify_recaptcha
assert_equal "reCAPTCHA verification failed, please try again.", @controller.flash[:recaptcha_error]
assert_equal "Recaptcha failure after api call. Api reply: {\"foo\"=>\"false\", \"bar\"=>\"invalid-site-secret-key\"}.", @controller.recaptcha_failure_reason
end

it "adds an error to the model" do
Expand All @@ -79,6 +97,7 @@ def initialize

assert @controller.verify_recaptcha(secret_key: key)
assert_nil @controller.flash[:recaptcha_error]
assert_nil @controller.recaptcha_failure_reason
end

it "returns true on success without remote_ip" do
Expand Down Expand Up @@ -304,6 +323,7 @@ def initialize
it "fails when score is below minimum_score" do
refute verify_recaptcha(minimum_score: 0.5)
assert_flash_error
assert_equal "Recaptcha score didn't exceed the minimum: 0.4 < 0.5.", @controller.recaptcha_failure_reason
end

it "fails when response doesn't include a score" do
Expand Down Expand Up @@ -387,6 +407,19 @@ def initialize
end
end

describe "recaptcha_failure_reason" do
let(:default_response_hash) { {
success: true,
score: 0.97,
'error-codes': ['some-api-error']
} }
it "contains the error-codes when reply has those" do
expect_http_post.to_return(body: success_body)
refute verify_recaptcha()
assert_equal "Recaptcha api call returned with error-codes: [\"some-api-error\"].", @controller.recaptcha_failure_reason
end
end

describe "#recaptcha_response_token" do
it "returns an empty string when params are empty and no action is provided" do
@controller.params = {}
Expand Down

0 comments on commit cc575f3

Please sign in to comment.