Is there a best practice for throttling login attempts in Phoenix?
What is everybody else doing right now?
Is there a best practice for throttling login attempts in Phoenix?
What is everybody else doing right now?
Check this package out https://github.com/grempe/ex_rated
We use it on one project and have our own (a genserver with simple state and some custom logic) on another.
One possible approach, assuming the login is database backed, is to also store it in the database. You need a column with the attempts and the time of the first attempt. Once login succeeds, you clear those. If login fails and it is within the initial timestamp, you bump the counter. The goal is to not allow more than 5 attempts in X minutes.
I recommend this approach as well, then you also have an audit trail as a side effect
def check_rate(ip, email) do
expiry = Timex.shift(Timex.now(), minutes: -15)
{:ok, ip} = EctoNetwork.INET.cast(ip)
query =
from a in Attempt,
where:
a.ip == ^ip and a.inserted_at >= ^expiry and
a.success == false and a.email == ^email
{:rate, Repo.aggregate(query, :count, :ip) < 5}
end
or similar depending on the requirements.
I implemented on registration and login form Recaptcha from Google:
Thanks, guys.
I’ll be rolling with a similar DB query version for now.
I’m late to the party, but we’ve been very pleased with this erlang library we’ve had in production for a few years now. We use it primarily for rate limiting our outgoing calls, but it works just as good for incoming calls. The mnesia backend it offers was our main reason to choose it for our 2 node cluster.
Yeah, in the long run, an ETS (or mnesia?) based approach that doesn’t query the database with every attempt would probably be better.
Here’s what I’ve got for now. (don’t ask me why I made options even though I’ll probably never not use the defaults, I don’t know, it’s a sickness )
def check_login_attempts(ip_address, email, opts \\ []) do
{limit, opts} = Keyword.pop(opts, :limit, 5)
if get_login_attempt_rate(ip_address, email, opts) < limit do
:ok
else
{:error, :too_many_requests}
end
end
def get_login_attempt_rate(ip_address, email, opts \\ []) do
time_ago_opts = Keyword.get(opts, :time_ago, [minutes: -1])
time_ago = Timex.shift(Timex.now(), time_ago_opts)
query =
from a in LoginAttempt,
where: a.ip_address == ^ip_address or a.email == ^email,
where: a.attempted_at >= ^time_ago,
where: not a.success
Repo.aggregate(query, :count)
end
I didn’t limit it to just ip_address
but also email
address (which is a login credential). In case, for example and for whatever reason, someone is using a botnet or something to attempt to login to a single account, not only will the IPs get throttled, the account itself will be as well.