ActAsTextcaptcha
ActsAsTextcaptcha provides spam protection for Rails models with text-based logic question captchas. Questions are fetched from Rob Tuley's textcaptcha.com. They can be solved easily by humans but are tough for robots to crack.
The gem can also be configured with your own questions; as an alternative, or as a fallback to handle any API issues. For reasons on why logic based captchas are a good idea visit textcaptcha.com.
Requirements
- Ruby >= 3.1
- Rails >= 6.1
- A valid Rails.cache (not
:null_store
)
Demo
Try a working demo here!
Installation
Add this line to your Gemfile and run bundle install
:
gem 'acts_as_textcaptcha'
Add this to models you'd like to protect:
class Comment < ApplicationRecord
acts_as_textcaptcha api_key: 'TEXTCAPTCHA_API_IDENTITY'
# see below for more config options
end
Rob requests that your
TEXTCAPTCHA_API_IDENTITY
be some reference to yourself (e.g. an email address,
domain or similar) so you can be contacted should any usage problem occur.
In your controller's new
action call the textcaptcha
method:
def new
@comment = Comment.new
@comment.textcaptcha
end
Make sure these textcaptcha params
(:textcaptcha_answer, :textcaptcha_key
) are permitted in your create (or update) action:
def create
@comment = Comment.create(commment_params)
# ... etc
end
private
def commment_params
params.require(:comment).permit(:textcaptcha_answer, :textcaptcha_key, :name, :comment_text)
end
NOTE: if the captcha is submitted incorrectly, a new captcha will be
automatically generated on the @comment
object.
Next, add the question and answer fields to your form using the
textcaptcha_fields
helper. Arrange the HTML within this block as you like.
<%= textcaptcha_fields(f) do %>
<div class="field">
<%= f.label :textcaptcha_answer, @comment.textcaptcha_question %><br/>
<%= f.text_field :textcaptcha_answer, :value => '' %>
</div>
<% end %>
If you'd prefer to construct your own form elements, take a look at the HTML produced here.
Finally set a valid cache
store (not :null_store
) for your environments:
# e.g. in config/environments/*.rb
config.cache_store = :memory_store
You can run rails dev:cache
on to enable a memory store cache in the development environment.
Configuration
The following options are available (only api_key
is required):
- api_key (required) - a reference to yourself (e.g. your email or domain).
- questions (optional) - array of your own questions and answers (see below).
- cache_expiry_minutes (optional) - how long valid answers are cached for (default 10 minutes).
- raise_errors (optional) - if true, API or network errors are raised (default false, errors logged).
- api_endpoint (optional) - set your own JSON API endpoint to fetch from (see below).
For example:
class Comment < ApplicationRecord
acts_as_textcaptcha api_key: 'TEXTCAPTCHA_API_IDENTITY_KEY',
raise_errors: false,
cache_expiry_minutes: 10,
questions: [
{ 'question' => '1+1', 'answers' => '2,two' },
{ 'question' => 'The green hat is what color?', 'answers' => 'green' }
]
end
YAML config
You can apply an app wide config with a config/textcaptcha.yml
file. Use this
rake task to add one from a
template:
$ bundle exec rake textcaptcha:config
NOTE: Any options set in models take preference over this config.
Config without the TextCaptcha service
To use only your own logic questions, omit the api_key
and set them in the
config (see above). Multiple answers to the same question must be comma
separated e.g. 2,two
(so do not include commas in answers).
You can optionally set your own api_endpoint
to fetch questions and answers
from. The URL must respond with a JSON object like this:
{
"q": "What number is 4th in the series 39, 11, 31 and nineteen?",
"a": ["1f0e3dad99908345f7439f8ffabdffc4","1d56cec552bf111de57687e4b5f8c795"]
}
With "a"
being an array of answer strings, MD5'd, and lower-cased. The
api_key
option is ignored if an api_endpoint
is set.
Toggling TextCaptcha
Enable or disable captchas by overriding the perform_textcaptcha?
method (in
models). By default the method checks if the object is a new (unsaved) record.
So spam protection is only enabled for creating new records (not updating).
Here's an example overriding the default behaviour but maintaining the new record check.
class Comment < ApplicationRecord
acts_as_textcaptcha :api_key => 'TEXTCAPTCHA_API_IDENTITY'
def perform_textcaptcha?
super && user.admin?
end
end
Translations
The following strings are translatable (with Rails I18n translations):
en:
activerecord:
errors:
models:
comment:
attributes:
textcaptcha_answer:
incorrect: "is incorrect, try another question instead"
expired: "was not submitted quickly enough, try another question instead"
activemodel:
attributes:
comment:
textcaptcha_answer: "TextCaptcha answer"
NOTE: The textcaptcha.com API only provides logic questions in English.
Handling Errors
The API may be unresponsive or return an unexpected response. If you've set
raise_errors: true
, consider handling these errors:
ActsAsTextcaptcha::ResponseError
ActsAsTextcaptcha::ParseError
ActsAsTextcaptcha::ApiKeyError
Development
Check out this repo and run bin/setup
, this will install gem dependencies and
generate docs. Use bundle exec rake
to run tests.
You can also run bin/console
for an interactive prompt to experiment with the
code.
Tests
MiniTest is used for testing. Run the test suite with:
$ rake test
This gem uses appraisal to test against multiple versions of Rails.
-
appraisal rake test
(run tests against all Gemfile variations) -
appraisal rails-3 rake test
(run tests against a specific Gemfile)
Docs
Generate docs for this gem with:
$ rake rdoc
Troubles?
If you think something is broken or missing, please raise a new issue. Please remember to check it hasn't already been raised.
Contributing
Bug reports and pull requests are welcome on GitHub. When submitting pull requests, remember to add tests covering any new behaviour, and ensure all tests are passing on Travis. Read the contributing guidelines for more details.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct. See here for more details.
Ideas
- Check if AI models can beat this approach
- Allow translatable user supplied questions and answers in config
- Allow
Net::HTTP
to be swapped out for any HTTP client.
License
The code is available as open source under the terms of LGPL-3.
Who's who?
- ActsAsTextcaptcha and little robot drawing by Matthew Hutchinson
- TextCaptcha API and service by Rob Tuley