If you launch a Rails app to the world, sooner than later, you will see them: spam sign ups (along with requests to typical WordPress URLS). The reasons bots sign up vary from: exploiting free trials and referral programs, (competitive) data scraping or testing stolen credit cards.
Your first sign ups will most likely not be potential customers, but bots. š„² Sorry, thatās how it is. But it is easy, at least for the first years of your business, to fight them with these techniques.
This article builds on top of this one: Add Sign Up to Rails 8ā Authentication.
Letās look at the Signup class from that article:
# app/models/signup.rb
class Signup
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email_address, :string
attribute :password, :string
validates :email_address, presence: true
validates :password, length: 8..128
def save
if valid?
User.create!(email_address: email_address, password: password).tap do |user|
create_workspace_for user
send_welcome_email_to user
end
end
end
# ā¦
end
I like classes (form objects) to āquackā like Rails, so letās extend it with a custom validator:
# app/models/signup.rb
class Signup
# ā¦
validates :email_address, presence: true
validates :password, length: 8..128
validates :email_address, is_not_spam: true
def save
# ā¦
end
This is_not_spam
validator can then be defined like so:
# app/validators/is_not_spam_validator.rb
class IsNotSpamValidator < ActiveModel::EachValidator
def validate_each(record, attribute, email_address)
# ā¦
end
end
There are two common types of emails bots use:
- disposable emails providers;
- Gmail with dots (Dots donāt matter in Gmail addresses; for non-organization email accounts).
Letās add a validation for disposable email providers first:
# app/validators/is_not_spam_validator.rb
class IsNotSpamValidator < ActiveModel::EachValidator
def validate_each(record, attribute, email_address)
return if email_address.blank?
if spam? email_address
record.errors.add(attribute, options[:message] || "appears suspicious")
end
end
private
def spam?(email_address)
local_part, domain = email_address.split("@", 2)
disposable?(domain)
end
def disposable?(domain) = disposable_email_providers.include?(domain)
def disposable_email_providers = File.read("config/disposable_email_providers.txt").split("\n")
end
I think this is all really good to follow, right? Whatās in config/disposable_email_providers.txt
? A list of disposable domains courtesy of this repo. Copy it and paste it in the text file (or create a little script that does it for you).
Next is the issue with Gmail treating johnsmith@gmail.com and j.o.h.n.s.m.i.t.h@gmail.com the same. Extend the spam?
method:
# app/validators/is_not_spam_validator.rb
class IsNotSpamValidator < ActiveModel::EachValidator
private
MAXIMUM_DOTS = 3
def spam?(email_address)
local_part, domain = email_address.split("@", 2)
disposable?(domain) || excessive_dots_in?(local_part)
end
def excessive_dots_in?(local_part) = local_part.count(".") >= MAXIMUM_DOTS
end
This is not specifically checking for excessive dots Ɣnd Gmail, but normally more than 3 dots is not common for any email domain.
With these two checks in place, you are already in a good place to fight spam. But there is one more, easy-to-add, piece I add from the start. A honeypot.
For this honeypot, I always use the invisible_captha gem. This is how I use it:
bundle add invisible_captha
- in SignupsController:
invisible_captcha only: %w[create], timestamp_enabled: false
- in app/views/signups/new.html.erb:
<%= invisible_captcha %>
And done. Check out the invisible_captcha repo for more details and documentation.
With these three checks, you surely will see almost no automated sign ups in your app. At least in the early stages of your app. š¬
Is there something you add to fight spam or anything I missed? Let me know.