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

The native backend: users and sessions in the host's database,
passwords checked by a chain of sources (local password hashes, LDAP
directories), sign-in failures throttled.

    config :macaw_auth, backend: Macaw.Auth.Native
    config :macaw_auth, Macaw.Auth.Native,
      repo: MyApp.Repo,
      sources: [%{type: :password}, %{type: :ldap, name: "nas", ...}],
      session_idle_days: 14

and `mix macaw_auth.gen.migration` for the tables. The pieces:

- `Macaw.Auth.Native.Config`: the configuration, parsed once, every
  fault named;
- `Macaw.Auth.Native.Source`: the chain (the first source that knows the
  user decides) and its two sources, `Password` and `Ldap`;
- `Macaw.Auth.Native.Provision`: directory users' rows, made at their
  first sign-in and their roles re-derived at every one;
- `Macaw.Auth.Native.Hasher`: bcrypt or argon2, rehashing at sign-in;
- `Macaw.Auth.Native.Throttle`: backoff after repeated failures, per
  username and per client address;
- `Macaw.Auth.Native.Sessions`: hashed session tokens, idle expiry,
  steps;
- `Macaw.Auth.Native.Totp`: the second factor, TOTP and recovery codes,
  its secret sealed at rest (`Macaw.Auth.Native.Seal`);
- `Macaw.Auth.Native.UserRecord`, `Macaw.Auth.Native.SessionRecord` and
  `Macaw.Auth.Native.RecoveryCodeRecord`: the `macaw_auth_users`,
  `macaw_auth_sessions` and `macaw_auth_recovery_codes` schemas.

**One refusal.** Whatever a source's reason, the caller sees
`:invalid_credentials`, or `:disabled` for a disabled user who gave
the right password, or `:throttled` while a wait runs. The specific
reason, and the source, go to telemetry
(`[:macaw_auth, :native, :authenticate]`) and to
`mix macaw_auth.ldap.check`.

**The second factor.** A user's `second_factor` is `:enrolled` once they
confirm a TOTP enrollment, else `:required` when policy
(`totp_required`) requires one, else `:none`; `Macaw.Auth.pending_step/2`
turns that into the step a session owes. The enrollment and
verification functions here put every code check behind the throttle:
a wrong code is a failure for the user's second-factor key and the
client's address, with the same backoff as a wrong password, so six
digits can't be guessed. The second factor's username key is its own
(a password success doesn't reset it, or each sign-in would buy a
guesser who has the password five more free guesses at the code); the
address key is the one sign-in uses. Configure it with:

    config :macaw_auth, Macaw.Auth.Native,
      totp_issuer: "My App",
      totp_required: ["admin"],            # or true, or false (the default)
      sealing_key: {MyApp.Secrets, :sealing_key, []}

# `admin_reset`

```elixir
@spec admin_reset(Macaw.Auth.User.t() | term(), keyword()) :: :ok
```

An administrator clears a user's second factor and revokes their sessions,
telling their open pages unless option `announce: false`
(`Macaw.Auth.Native.Totp.admin_reset/2`).

# `authenticate`

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

Checks `%{username: _, password: _}` through the source chain:
`{: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).

**Throttled.** While the username or the address waits
(`Macaw.Auth.Native.Throttle`), the password is not checked, but a
hash's time is still spent (the hasher's `no_user_verify/0`), so a
blocked attempt takes as long as a checked one and the response time
doesn't tell a guesser when the block starts or ends. A throttled
refusal is not counted; every checked refusal counts as a failure, and
a success resets the username's count.

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

# `bootstrap_admin`

```elixir
@spec bootstrap_admin(map()) ::
  {:ok, Macaw.Auth.Native.UserRecord.t()}
  | :exists
  | {:error, :no_password | :no_role | Ecto.Changeset.t()}
```

Makes the first user, a local one with role `:role`, **only when no user
exists**: `{:ok, record}` when it made one, `:exists` when any user is
there (local or a directory's, whoever it is), so it is safe to call at
every boot. Takes `:username`, `:password` and `:role`, and optionally
`:display_name` and `:must_change_password`.

With no user yet, the password and the role are required:
`{:error, :no_password}`, `{:error, :no_role}`, or `{:error, changeset}`
(an invalid username). When users exist neither is looked at, so a host
may stop providing the password once its admin exists.

Two first boots at once with the same username make one user: the other
call's insert meets the unique index and answers `:exists`.

# `check_config!`

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

Reads and checks the configuration, and keeps it
(`Macaw.Auth.Native.Config.load!/0`): `:ok`, or
`Macaw.Auth.Native.Config.Error` naming the fault. The application
calls it at start, so a bad configuration refuses boot.

# `create_user`

```elixir
@spec create_user(map()) ::
  {:ok, Macaw.Auth.Native.UserRecord.t()} | {:error, Ecto.Changeset.t()}
```

Makes a local user (`:username`, `:password`, and optionally
`:display_name`, `:roles`, `:must_change_password`, `:extra`), the
password hashed with the configured hasher: `{:ok, record}` or
`{:error, changeset}`.

# `disable`

```elixir
@spec disable(Macaw.Auth.User.t(), term(), keyword()) ::
  :ok | {:error, :invalid_code | :throttled | :required}
```

The user turns their second factor off with a current code or a recovery
code, behind the throttle: `:ok`, or `{:error, :invalid_code |
:throttled | :required}`.

# `disable_user`

```elixir
@spec disable_user(String.t()) ::
  {:ok, Macaw.Auth.Native.UserRecord.t()} | {:error, :not_found}
```

Disables a user: their sign-in is refused, and each of their sessions at
its next look-up (`Macaw.Auth.Native.Sessions`), so open pages leave at
their next re-check. A user already disabled keeps when it was first
done. `{:ok, record}` or `{:error, :not_found}`.

# `enable_user`

```elixir
@spec enable_user(String.t()) ::
  {:ok, Macaw.Auth.Native.UserRecord.t()} | {:error, :not_found}
```

Enables a disabled user again: `{:ok, record}` or `{:error, :not_found}`.

# `enroll_begin`

```elixir
@spec enroll_begin(Macaw.Auth.User.t(), keyword()) ::
  {:ok, %{secret: binary(), uri: String.t()}}
  | {:error, :already_enrolled | :not_found}
```

Starts (or resumes) `user`'s TOTP enrollment: `{:ok, %{secret, uri}}`,
the `otpauth://` URI for the page's QR code. Not active until
`enroll_confirm/3`. See `Macaw.Auth.Native.Totp.enroll_begin/2`.

# `enroll_confirm`

```elixir
@spec enroll_confirm(Macaw.Auth.User.t(), term(), keyword()) ::
  {:ok, [String.t()]}
  | {:error, :invalid_code | :throttled | :not_started | :already_enrolled}
```

Confirms the enrollment with a valid code, behind the throttle:
`{:ok, recovery_codes}` to show once, or `{:error, :invalid_code |
:throttled | :not_started | :already_enrolled}`. A session that owed
`:second_factor_enroll` then completes it (`Macaw.Auth.Plug.complete_step/2`).

# `get_user`

```elixir
@spec get_user(term()) :: Macaw.Auth.Native.UserRecord.t() | nil
```

The user named `username` (trimmed and lower-cased, as sign-in matches
it), or nil, also for a string that can't be a username.

# `list_users`

```elixir
@spec list_users() :: [Macaw.Auth.Native.UserRecord.t()]
```

Every user, local and directory, by username.

# `recovery_codes_left`

```elixir
@spec recovery_codes_left(Macaw.Auth.User.t() | term()) :: non_neg_integer()
```

How many unspent recovery codes the user has.

# `regenerate_recovery_codes`

```elixir
@spec regenerate_recovery_codes(Macaw.Auth.User.t(), keyword()) ::
  {:ok, [String.t()]} | {:error, :not_enrolled}
```

Replaces an enrolled user's recovery codes: `{:ok, codes}` or `{:error, :not_enrolled}`.

# `session_idle_seconds`

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

The idle limit, `session_idle_days` in seconds.

# `set_password`

```elixir
@spec set_password(String.t(), term(), keyword()) ::
  {:ok, Macaw.Auth.Native.UserRecord.t()}
  | {:error, :not_found | :not_local | :no_password}
```

Replaces a local user's password, hashed with the configured hasher, and
revokes every session of theirs (reason `:password_changed`), so whoever
held the old one is signed out. `{:ok, record}`, or `{:error, :not_found
| :not_local | :no_password}`: a directory user's password is the
directory's, and is never set here.

Option `:announce` (default `true`): tell the user's open pages, which
disconnect (`Macaw.Auth.announce_revoked/2`). A process that is not the
app's node, such as a release's `eval` or a mix task, passes `false`:
there is no page there to tell, and the pages leave at their next
re-check.

# `set_roles`

```elixir
@spec set_roles(String.t(), [String.t()]) ::
  {:ok, Macaw.Auth.Native.UserRecord.t()}
  | {:error, :not_found | :not_local | :invalid_roles}
```

Replaces a local user's roles with `roles`, a list of non-empty strings:
`{:ok, record}`, or `{:error, :not_found | :not_local | :invalid_roles}`.
A directory user's roles are re-derived from the directory at each
sign-in (`Macaw.Auth.Native.Provision`), so they are never set here.

# `to_user`

```elixir
@spec to_user(Macaw.Auth.Native.UserRecord.t()) :: Macaw.Auth.User.t()
```

A users row as the contract's `%Macaw.Auth.User{}`, its `second_factor`
from the row and the configured policy.

# `verify_second_factor`

```elixir
@spec verify_second_factor(term(), term(), keyword()) ::
  :ok | {:error, :invalid_code | :throttled | :invalid | :expired | :disabled}
```

Checks a second-factor code (TOTP, or a recovery code) for the user of
session `session_id`, behind the throttle; on success the session's
`:second_factor` step is completed. `:ok`, or `{:error, :invalid_code |
:throttled}`, or the session's own refusal (`:invalid | :expired |
:disabled`). Options: `:remote_ip`, `:now`.

---

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