Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add recaptcha_failure_reason for enchanced error reporting #459

Merged
merged 2 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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