# `MishkaCmsCore.Accounts.AuthRateLimit`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/accounts/_auth_rate_limit.ex#L5)

How often someone may sign in with a password, ask for a password reset, ask for a sign-in link
and try their current password to change it: the backend and the keys
`MishkaCmsCore.Accounts.User` gives `ash_rate_limiter` for those four actions, and what to tell
whoever was refused.

The first three are counted twice, per IP address and per account; a registration once, per IP
address; a password change once, per signed-in account. Every attempt counts, right or wrong. The DSL numbers are the defaults:

| action | per IP address | per account | setting |
|--------|----------------|-------------|---------|
| `sign_in_with_password` | 30 per 5 minutes | 10 per 15 minutes | `security.sign_in_per_*` |
| `request_password_reset_token` | 10 per hour | 3 per hour | `security.reset_request_per_*` |
| `request_magic_link` | 10 per hour | 3 per hour | `security.magic_link_per_*` |
| `register_with_password` | 10 per hour | — | `security.register_per_ip` |
| `register_with_provider`, every sign-in provider's | 30 per 5 minutes | — | `security.sign_in_per_ip` |
| `change_password` | — | 10 per 15 minutes | `security.change_password_per_account` |
| `verify_with_totp`, `confirm_setup_with_totp`, `verify_with_recovery_code` | — | 5 per 5 minutes, together | `security.totp_per_account` |

A master changes the numbers on the Settings page (`MishkaCmsCore.Settings`); the windows stay.
`hit/3` reads the setting from the settings cache on every attempt, so a change takes effect on
the next one. `config :mishka_cms_core, MishkaCmsCore.Accounts.AuthRateLimit, limit: n` holds every action to
`n` instead, for a test suite that signs in from one address.

Every site is counted apart, masters together as one more. An account is its email, or its id
once signed in; the address is the one `RemoteIp` reads with `remote_ip_options/0`, from the
`ash_authentication_request` context: AshAuthentication writes it on the browser's `/api/auth`
routes, `MishkaCmsCore.Runtime.Plugs.AuthRateLimitPlug` on the JSON:API and the sign-in pages'
forms from `MishkaCmsWeb.RuntimeLive`'s `@client_request`. An IPv6 client is counted by its /64,
and an IPv4 address written as IPv6 (`::ffff:192.0.2.7`) as that IPv4 address. An action run
without it is counted under the address `"unknown"`.

# `limited`

```elixir
@type limited() :: %{
  action:
    :sign_in
    | :reset_request
    | :magic_link
    | :register
    | :change_password
    | :totp,
  key: :ip | :account,
  retry_after: pos_integer()
}
```

# `by_account`

```elixir
@spec by_account(Ash.Query.t() | Ash.ActionInput.t(), map()) :: String.t()
```

The key an attempt is counted under by the account it names, within its site:
`"auth:<action>:account:<site id | master>:<digest of the email>"`.

# `by_address`

```elixir
@spec by_address(Ash.Query.t() | Ash.ActionInput.t() | Ash.Changeset.t(), map()) ::
  String.t()
```

The key an attempt is counted under by the address it came from, within its site:
`"auth:<action>:ip:<site id | master>:<address>"`. The address of an IPv6 client is its /64
(`"2001:db8:1:2::/64"`), and an IPv4 address written as IPv6 is that IPv4 address.

# `by_user`

```elixir
@spec by_user(Ash.Changeset.t() | Ash.ActionInput.t(), map()) :: String.t()
```

The key a signed-in account's attempt on itself is counted under, within its site:
`"auth:<action>:account:<site id | master>:<user id>"`. An action input names its account in the
`:user` argument, as `verify_with_totp` does.

# `describe`

```elixir
@spec describe(limited()) :: %{title: String.t(), text: String.t()}
```

What to tell whoever was refused: a title naming what they asked for too often, and how long to
wait.

    AuthRateLimit.describe(%{action: :sign_in, key: :account, retry_after: 90})
    #=> %{title: "Too many sign-in attempts", text: "Try again in 2 minutes."}

# `hit`

Counts one hit on `key` in `MishkaCmsCore.Runtime.States.RateLimitState`, held to the setting
`"security.<action>_per_<ip|account>"` the key names, or `limit` while that holds no positive
integer.

# `limited`

```elixir
@spec limited(term()) :: limited() | nil
```

The refusal anywhere inside `reason`, or `nil`: an error or error class, a form's `raw_errors`,
an `AshAuthentication.Errors.AuthenticationFailed`, or the JSON:API errors document written for
one.

    AuthRateLimit.limited(Form.raw_errors(form, for_path: :all))
    #=> %{action: :sign_in, key: :account, retry_after: 812}

# `remote_ip_options`

```elixir
@spec remote_ip_options() :: keyword()
```

The options `RemoteIp` reads the client's address with: `x-forwarded-for`, right to left, passing
over private addresses and `config :mishka_cms_core, :trusted_proxies` (IPs or CIDR ranges), but
never over Cloudflare Workers' address for a request to another zone, `2a06:98c0:3600::103`. The
endpoint's `plug RemoteIp` and the `/auth` socket take the same ones.

# `validate_trusted_proxies!`

```elixir
@spec validate_trusted_proxies!() :: :ok
```

Reads `config :mishka_cms_core, :trusted_proxies` the way every request does, and raises
`ArgumentError` naming the first entry that is no IP or CIDR range. Call it before the endpoint
starts, so a mistyped `TRUSTED_PROXY_RANGES` stops the boot.

---

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