# `Macaw.Auth.Native.Throttle`
[🔗](https://github.com/dcoai/macaw_auth/blob/v0.4.1/lib/macaw/auth/native/throttle.ex#L1)

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.RateLimit` limits 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.

# `key`

```elixir
@type key() :: {:user, user()} | {:ip, String.t()}
```

# `user`

```elixir
@type user() :: String.t() | :invalid | {:second_factor, String.t()}
```

A user key holds a normalized username, `:invalid`, or
`{:second_factor, username}` (code attempts, counted apart from
passwords: `Macaw.Auth.Native`).

# `check`

```elixir
@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`.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `delay_ms`

```elixir
@spec delay_ms(non_neg_integer()) :: non_neg_integer()
```

The wait after `failures` consecutive failures, in ms.

# `failure`

```elixir
@spec failure(user(), String.t() | nil, integer()) :: :ok
```

Records a failure for `username` and `ip` at `now`.

# `now`

```elixir
@spec now() :: integer()
```

The system clock in ms, the time every caller passes as `now`.

# `reset`

```elixir
@spec reset() :: :ok
```

Forgets everything (tests, and an operator's reset).

# `start_link`

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.

# `success`

```elixir
@spec success(user()) :: :ok
```

A success resets the username's count (not the address's).

# `sweep`

```elixir
@spec sweep(integer()) :: non_neg_integer()
```

Forgets keys whose last failure is older than a day at `now`; returns how many.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
