Sign-in throttling for Macaw.Auth.Native.authenticate/2 (ported from
exio's and macaw's Accounts.Throttle, which are the same mechanism).
Failures are counted per normalized username and per client address.
The first five are free; after n consecutive failures (n ≥ 5) a key
waits 2^(n-5) seconds from its last failure, capped at 15 minutes, and
an attempt inside the wait is refused (:throttled) without checking
the password. Nothing is ever locked for good: the right password
succeeds once the wait has passed, so a guesser can't lock a user out
for longer than the cap.
- Both keys. The username key slows guessing one account from many addresses; the address key slows one address trying many accounts. An attempt waits for the longer of the two.
- A success resets the username only. Resetting the address too would let a guesser who holds one valid account clear their address's count between guesses at others.
- No address, no address key. A caller that passes no
:remote_ip(a console task) is throttled by username alone, rather than sharing one "unknown address" key with every other such caller. - Idle keys are swept. A key whose last failure is more than a day old is forgotten, so the table holds recent failures only, whatever an attacker sends.
- Not a request rate limit.
Macaw.Plug.RateLimitlimits requests per window; this is a different policy (backoff after failures, reset by success), so it is its own small table rather than a configuration of that one.
The counts live in a public ETS table owned by this process, which the
macaw_auth application starts. Every function takes the time as now,
in milliseconds of the system clock, so tests drive it without
sleeping. The values are constants: mechanism, not a host's policy.
Summary
Types
A user key holds a normalized username, :invalid, or
{:second_factor, username} (code attempts, counted apart from
passwords: Macaw.Auth.Native).
Functions
:ok, or {:delay, ms} left before username or ip may try again
at now.
Returns a specification to start this module under a supervisor.
The wait after failures consecutive failures, in ms.
Records a failure for username and ip at now.
The system clock in ms, the time every caller passes as now.
Forgets everything (tests, and an operator's reset).
Starts the table's owner. Options: :sweep_every_ms (default ten
minutes) and :clock (a zero-arity function giving now in ms, default
the system clock), for the periodic sweep.
A success resets the username's count (not the address's).
Forgets keys whose last failure is older than a day at now; returns how many.
Types
A user key holds a normalized username, :invalid, or
{:second_factor, username} (code attempts, counted apart from
passwords: Macaw.Auth.Native).
Functions
@spec check(user(), String.t() | nil, integer()) :: :ok | {:delay, pos_integer()}
:ok, or {:delay, ms} left before username or ip may try again
at now.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec delay_ms(non_neg_integer()) :: non_neg_integer()
The wait after failures consecutive failures, in ms.
Records a failure for username and ip at now.
@spec now() :: integer()
The system clock in ms, the time every caller passes as now.
@spec reset() :: :ok
Forgets everything (tests, and an operator's reset).
Starts the table's owner. Options: :sweep_every_ms (default ten
minutes) and :clock (a zero-arity function giving now in ms, default
the system clock), for the periodic sweep.
@spec success(user()) :: :ok
A success resets the username's count (not the address's).
@spec sweep(integer()) :: non_neg_integer()
Forgets keys whose last failure is older than a day at now; returns how many.