From 162f80e77867e62753b7a51fabf1b6b2cbd11f61 Mon Sep 17 00:00:00 2001 From: Artem Skrynnyk Date: Tue, 14 Jul 2026 01:15:11 +0300 Subject: [PATCH 1/4] Add clear_invisible_captcha_session method --- README.md | 31 +++++++++++++++++++++++++ lib/invisible_captcha/controller_ext.rb | 5 ++++ 2 files changed, 36 insertions(+) diff --git a/README.md b/README.md index d855424..d30a7b5 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,37 @@ The `invisible_captcha` method accepts some options: - `timestamp_threshold`: custom threshold per controller/action. Overrides the global value for `InvisibleCaptcha.timestamp_threshold`. - `prepend`: the spam detection will run in a `prepend_before_action` if `prepend: true`, otherwise will run in a `before_action`. +### Clearing invisible_captcha session data + +`invisible_captcha` stores a couple of values in the session (timestamp and/or spinner token) when the form is rendered. +They get cleaned up automatically once the guarded action runs. +But if a form is rendered in one flow and the user completes a *different* action that isn't guarded by `invisible_captcha`, those keys are never consumed and stay in the session. +This can happen when: + +- a sign-up form sets these session keys, but the user logs in instead. +- a page renders a password-based login form alongside an OmniAuth/OAuth button (e.g.: "Sign in with Google"). + The form sets the session keys on render, but the user authenticates via the OAuth callback action instead, which never touches `invisible_captcha`. + +If you want to clear them explicitly, call `clear_invisible_captcha_session` at the exact point where you know the alternate flow succeeded: + +```ruby +class SessionsController < ApplicationController + def create + if @user = User.authenticate(params) + sign_in(@user) + clear_invisible_captcha_session + redirect_to root_path + else + render :new + end + end +end +``` + +Apply the same pattern in an OmniAuth callback controller's success branch. + +**Don't** wire this up as a `before_action`/`after_action`. There is no reliable, generic way to detect "the action succeeded" from a filter (a failed login might re-render the same action instead of redirecting, which would wipe out the freshly-rendered form's session values before the user gets a chance to resubmit). Call it explicitly, only on the success path you control. + ### View helpers options: Using the view/form helper you can override some defaults for the given instance. Actually, it allows to change: diff --git a/lib/invisible_captcha/controller_ext.rb b/lib/invisible_captcha/controller_ext.rb index cad5bb1..3fb5aa7 100644 --- a/lib/invisible_captcha/controller_ext.rb +++ b/lib/invisible_captcha/controller_ext.rb @@ -18,6 +18,11 @@ def invisible_captcha(options = {}) private + def clear_invisible_captcha_session + session.delete(:invisible_captcha_timestamp) + session.delete(:invisible_captcha_spinner) + end + def detect_spam(options = {}) if timestamp_spam?(options) on_timestamp_spam(options) From 8ff8e06b7731d310b27f4a90e22b9fdcb893aac9 Mon Sep 17 00:00:00 2001 From: Artem Skrynnyk Date: Tue, 14 Jul 2026 01:23:08 +0300 Subject: [PATCH 2/4] Extract session keys into constants --- lib/invisible_captcha.rb | 3 +++ lib/invisible_captcha/controller_ext.rb | 8 ++++---- lib/invisible_captcha/view_helpers.rb | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/invisible_captcha.rb b/lib/invisible_captcha.rb index e447108..040806d 100644 --- a/lib/invisible_captcha.rb +++ b/lib/invisible_captcha.rb @@ -7,6 +7,9 @@ require 'invisible_captcha/railtie' module InvisibleCaptcha + SESSION_TIMESTAMP_KEY = :invisible_captcha_timestamp + SESSION_SPINNER_KEY = :invisible_captcha_spinner + class << self attr_writer :sentence_for_humans, :timestamp_error_message diff --git a/lib/invisible_captcha/controller_ext.rb b/lib/invisible_captcha/controller_ext.rb index 3fb5aa7..fcd474d 100644 --- a/lib/invisible_captcha/controller_ext.rb +++ b/lib/invisible_captcha/controller_ext.rb @@ -19,8 +19,8 @@ def invisible_captcha(options = {}) private def clear_invisible_captcha_session - session.delete(:invisible_captcha_timestamp) - session.delete(:invisible_captcha_spinner) + session.delete(InvisibleCaptcha::SESSION_TIMESTAMP_KEY) + session.delete(InvisibleCaptcha::SESSION_SPINNER_KEY) end def detect_spam(options = {}) @@ -60,7 +60,7 @@ def timestamp_spam?(options = {}) return false unless enabled - timestamp = session.delete(:invisible_captcha_timestamp) + timestamp = session.delete(InvisibleCaptcha::SESSION_TIMESTAMP_KEY) # Consider as spam if timestamp not in session, cause that means the form was not fetched at all unless timestamp @@ -84,7 +84,7 @@ def spinner_spam? return false unless InvisibleCaptcha.spinner_enabled spinner_value = params[:spinner] - session_value = session.delete(:invisible_captcha_spinner) + session_value = session.delete(InvisibleCaptcha::SESSION_SPINNER_KEY) if spinner_value.blank? || !secure_compare(spinner_value, session_value) warn_spam("Spinner value mismatch") diff --git a/lib/invisible_captcha/view_helpers.rb b/lib/invisible_captcha/view_helpers.rb index 73cac5f..8b4724c 100644 --- a/lib/invisible_captcha/view_helpers.rb +++ b/lib/invisible_captcha/view_helpers.rb @@ -14,11 +14,11 @@ def invisible_captcha(honeypot = nil, scope = nil, options = {}) @captcha_ocurrences += 1 if InvisibleCaptcha.timestamp_enabled || InvisibleCaptcha.spinner_enabled - session[:invisible_captcha_timestamp] = Time.zone.now.iso8601 + session[InvisibleCaptcha::SESSION_TIMESTAMP_KEY] = Time.zone.now.iso8601 end if InvisibleCaptcha.spinner_enabled && @captcha_ocurrences == 1 - session[:invisible_captcha_spinner] = InvisibleCaptcha.encode("#{session[:invisible_captcha_timestamp]}-#{request.remote_ip}") + session[InvisibleCaptcha::SESSION_SPINNER_KEY] = InvisibleCaptcha.encode("#{session[InvisibleCaptcha::SESSION_TIMESTAMP_KEY]}-#{request.remote_ip}") end build_invisible_captcha(honeypot, scope, options) @@ -58,7 +58,7 @@ def build_invisible_captcha(honeypot = nil, scope = nil, options = {}) concat text_field_tag(build_input_name(honeypot, scope), nil, default_honeypot_options.merge(options)) end if InvisibleCaptcha.spinner_enabled - concat hidden_field_tag("spinner", session[:invisible_captcha_spinner], id: nil) + concat hidden_field_tag("spinner", session[InvisibleCaptcha::SESSION_SPINNER_KEY], id: nil) end end end From e787c2d083a4d0a59cdfae14d110b435395c08cb Mon Sep 17 00:00:00 2001 From: Artem Skrynnyk Date: Tue, 14 Jul 2026 01:25:24 +0300 Subject: [PATCH 3/4] Ignore coverage output and exclude specs from coverage report --- .gitignore | 1 + spec/spec_helper.rb | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 76f7a13..ef9d257 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ spec/dummy/log/*.log spec/dummy/tmp/ .byebug_history .DS_Store +/coverage/ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 484e33f..1db3390 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,9 @@ require 'simplecov-cobertura' SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter end -SimpleCov.start +SimpleCov.start do + add_filter '/spec/' +end require File.expand_path("../dummy/config/environment.rb", __FILE__) require 'rspec/rails' From 85def8d69c6db07e09964167beea35f1bffbd579 Mon Sep 17 00:00:00 2001 From: Artem Skrynnyk Date: Tue, 14 Jul 2026 11:36:20 +0300 Subject: [PATCH 4/4] Move clear_invisible_captcha_session method out of private scope --- lib/invisible_captcha/controller_ext.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/invisible_captcha/controller_ext.rb b/lib/invisible_captcha/controller_ext.rb index fcd474d..29dd277 100644 --- a/lib/invisible_captcha/controller_ext.rb +++ b/lib/invisible_captcha/controller_ext.rb @@ -16,13 +16,13 @@ def invisible_captcha(options = {}) end end - private - def clear_invisible_captcha_session session.delete(InvisibleCaptcha::SESSION_TIMESTAMP_KEY) session.delete(InvisibleCaptcha::SESSION_SPINNER_KEY) end + private + def detect_spam(options = {}) if timestamp_spam?(options) on_timestamp_spam(options)