From cc13872f04bfe8c762c2087462da6a94ac8a586d Mon Sep 17 00:00:00 2001 From: Olli Huotari Date: Sun, 8 Dec 2024 21:22:58 +0200 Subject: [PATCH 1/2] Saving reason for recaptcha failure and having better exception messages --- lib/recaptcha/adapters/controller_methods.rb | 23 +++++++++++++- test/verify_test.rb | 33 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/recaptcha/adapters/controller_methods.rb b/lib/recaptcha/adapters/controller_methods.rb index b9b962a..8292755 100644 --- a/lib/recaptcha/adapters/controller_methods.rb +++ b/lib/recaptcha/adapters/controller_methods.rb @@ -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] @@ -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 @@ -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, @@ -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) diff --git a/test/verify_test.rb b/test/verify_test.rb index 2fb9953..4fddd72 100644 --- a/test/verify_test.rb +++ b/test/verify_test.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 = {} From 161c4c4c868adcb5ba0378305e1d3e06fd31213c Mon Sep 17 00:00:00 2001 From: Olli Huotari Date: Sun, 8 Dec 2024 21:30:13 +0200 Subject: [PATCH 2/2] Added recaptcha_faliure_reason to Readme and changelog --- CHANGELOG.md | 1 + README.md | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 597f228..280c210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/README.md b/README.md index 28a1ed7..1bb7e55 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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. @@ -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)