# `Macaw.Auth.Ash`
[🔗](https://github.com/dcoai/macaw_auth_ash/blob/v0.3.2/lib/macaw/auth/ash.ex#L1)

A `Macaw.Auth` backend over AshAuthentication: a host's Ash user and
token resources behind the contract, so an Ash host gets Macaw's plugs,
LiveView hooks, steps and revocation push without a second user table.

    config :macaw_auth, backend: Macaw.Auth.Ash
    config :macaw_auth, Macaw.Auth.Ash,
      user_resource: MyApp.Accounts.User,
      strategies: [:password],
      roles_from: :roles,
      disabled_field: :disabled_at

(`Macaw.Auth.Ash.Config` has every key.) The token resource takes
`Macaw.Auth.Ash.TokenResource` and `Macaw.Auth.Ash.Notifier`; a
directory sign-in is the `Macaw.Auth.Ash.Strategy.Ldap` strategy on the
user resource.

- **Who someone is**: `authenticate/2` runs the configured
  AshAuthentication strategies in order, as `Macaw.Auth.Native` runs its
  sources: the first that knows the user decides; failures back off by
  macaw_auth's native throttle.
- **Their sessions**: stored user tokens in the token resource
  (`Macaw.Auth.Ash.Sessions`, which says why).

**The second factor is unsupported.** AshAuthentication 4.x, the stable
line this backend targets, has no TOTP strategy (5.0, a release
candidate as this is written, adds one). So every user reports
`second_factor: :none`, no session owes the step, and
`verify_second_factor/3` refuses with `{:error, :unsupported}`. The
conformance kit runs with its second-factor block pending, saying why,
rather than passed by a stub. A host that needs a second factor today
uses `Macaw.Auth.Native`.

# `authenticate`

```elixir
@spec authenticate(map(), keyword()) ::
  {:ok, Macaw.Auth.User.t()}
  | {:error, :invalid_credentials | :disabled | :throttled}
```

Checks `%{username: _, password: _}` against the configured strategies,
in order: `{:ok, %Macaw.Auth.User{}}`, or `{:error, :invalid_credentials
| :disabled | :throttled}`. Options: `:remote_ip` (a string or an
address tuple; the client's, for the throttle's address key), `:now` (a
`DateTime`, for the throttle's clock).

**The first strategy that knows the user decides.** A password
strategy knows a user who has a row with a password hash; an LDAP
strategy, one its directory knows (`Macaw.Auth.Native.Source.Ldap.knows?/2`).
A refusal by a strategy that knows the user is final: a wrong password
never falls through to the next strategy, or one account's password
could be tried against another's. One that doesn't passes the attempt
on.

**One refusal.** A wrong password and an unknown user are both
`:invalid_credentials`, and take the same time: AshAuthentication's
password sign-in hashes against nothing when no user matches
(`hash_provider.simulate/0`, bcrypt's `no_user_verify`), as for a wrong
password it hashes against the user's; an empty username or password,
which never reaches the sign-in query, is hashed against nothing here.
A disabled user (`disabled_field`) is refused with `:disabled`, only
after the right password, so the refusal tells a guesser nothing.

**Throttled** as the native backend is, by its throttle
(`Macaw.Auth.Native.Throttle`, which macaw_auth's application starts
whichever backend is configured): failures back off per username
(trimmed and case-folded; an empty one shares one key) and per
`:remote_ip`. While either waits, the attempt is refused `:throttled`
without checking the password, and not counted, but a hash's time is
still spent (as for an empty field), so the response time doesn't tell
a guesser when the block starts or ends. Every other refusal counts as
a failure; a success resets the username's count, never the address's.

Emits `[:macaw_auth, :ash, :authenticate]` with `%{count: 1}` and
`%{result: :ok | :error, reason: term, strategy: name | nil, user_id:
id | nil}`; never the username or password.

# `check_config!`

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

Checks the whole configuration (`Macaw.Auth.Ash.Config.get/0`: the
resources, the strategies and every LDAP strategy's fields, the
notifier, `idle_days` against the token lifetime): `:ok`, or
`ArgumentError` naming the fault. macaw_auth's application calls it at
start, so a misconfigured host refuses boot rather than failing its
first sign-in.

# `session_idle_seconds`

```elixir
@spec session_idle_seconds(keyword()) :: pos_integer()
```

The idle limit `fetch_session/2` expires sessions by: `idle_days` in
seconds, which is also the remember cookie's lifetime.

# `to_user`

```elixir
@spec to_user(struct(), Macaw.Auth.Ash.Config.t()) :: Macaw.Auth.User.t()
```

A user record as the contract's `%Macaw.Auth.User{}`: the username from
`username_field`, roles from `roles_from`, the source from
`source_field` (else `"local"`), disabled from `disabled_field` (true,
or any timestamp), and no second factor (see the moduledoc).

# `verify_second_factor`

Unsupported: `{:error, :unsupported}`. AshAuthentication 4.x has no TOTP
strategy; users never owe a second factor under this backend (see the
moduledoc).

---

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