Skip to content
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ spec/dummy/log/*.log
spec/dummy/tmp/
.byebug_history
.DS_Store
/coverage/
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions lib/invisible_captcha.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions lib/invisible_captcha/controller_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ def invisible_captcha(options = {})
end
end

def clear_invisible_captcha_session
Comment thread
markets marked this conversation as resolved.
session.delete(InvisibleCaptcha::SESSION_TIMESTAMP_KEY)
session.delete(InvisibleCaptcha::SESSION_SPINNER_KEY)
end

private

def detect_spam(options = {})
Expand Down Expand Up @@ -55,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
Expand All @@ -79,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")
Expand Down
6 changes: 3 additions & 3 deletions lib/invisible_captcha/view_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading