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

The native backend's second factor: TOTP and recovery codes (ported from
exio's and macaw's `Accounts.Totp`, which are the same rules), on the
`macaw_auth_users` row and in `macaw_auth_recovery_codes`.

Call these through `Macaw.Auth.Native` (`verify_second_factor/3`,
`enroll_confirm/3`, `disable/3`), which puts every code check behind the
throttle; the functions here don't throttle.

- **RFC 6238**, through `NimbleTOTP`: SHA-1, six digits, 30-second
  steps; a 160-bit (20-byte) random secret; a code is accepted for the
  current step and one either side, so a clock a little off still
  works.
- **A code works once.** The step a code was accepted for is stored
  (`totp_last_step`), and a code is accepted only for a later step. The
  write is conditional (`UPDATE … WHERE totp_last_step < step`,
  `claim_step/3`), so of two submissions of one code racing each other,
  the database lets exactly one through; the other updates no row and
  is refused. Codes are compared with `Plug.Crypto.secure_compare/2`.
- **The secret is sealed** (`Macaw.Auth.Native.Seal`, AES-256-GCM under
  the configured `sealing_key`, bound to the user's id). It leaves this
  module only from `enroll_begin/2`, for the enrollment page's QR code.
- **Enrollment is pending until confirmed.** `enroll_begin/2` stores a
  sealed secret with `totp_enabled_at` nil, and returns the same secret
  on a reload; `enroll_confirm/3` with a valid code enables it and
  returns ten recovery codes, shown once.
- **Recovery codes**: ten, each ten characters of an alphabet without
  look-alikes (no 0, O, 1 or I), shown in two groups of five
  (`ABCDE-FGHJK`), stored only as SHA-256, each spent by deleting its
  row conditionally. Regenerating replaces them all.
- **Input** is normalized: spaces and dashes removed, letters upper
  case.
- **Policy** (`totp_required`, `Macaw.Auth.Native.Config`) decides
  `status/2`: `:enrolled`, else `:required` when policy requires one,
  else `:none`.

Every function honors `:now` (a `DateTime`). Telemetry
`[:macaw_auth, :native, :second_factor]`: `%{count: 1}` with
`%{event: :enrollment_started | :enrolled | :verified |
:recovery_code_used | :refused | :disabled | :reset |
:recovery_codes_regenerated, user_id: id}`; never a secret or a code.

# `user`

```elixir
@type user() :: Macaw.Auth.User.t() | Macaw.Auth.Native.UserRecord.t() | Ecto.UUID.t()
```

# `admin_reset`

```elixir
@spec admin_reset(user(), keyword()) :: :ok
```

An administrator clears a user's second factor (a lost device), enrolled
or pending, and revokes every session of theirs
(`Macaw.Auth.Native.Sessions.revoke_all/3`, so their open pages
disconnect): they sign in again, and enroll again if policy requires.
`:ok`. Option `:announce` (default `true`): `false` revokes without
telling open pages, for a process that is not the app's node.

# `claim_step`

```elixir
@spec claim_step(module(), Ecto.UUID.t(), integer()) :: :ok | :replayed
```

Records `step` as the user's last accepted one, only if it is later than
the one stored: `:ok`, or `:replayed` when it isn't (another submission
of the same code, or of an earlier one, got there first). The guard is
the update's own `WHERE`, so it holds however many callers race.

# `code`

```elixir
@spec code(binary(), integer() | DateTime.t()) :: String.t()
```

The six-digit code for `secret` at `time` (unix seconds or a `DateTime`).

# `disable`

```elixir
@spec disable(user(), term(), keyword()) :: :ok | {:error, :invalid_code | :required}
```

The user turns their second factor off, proving it with a current code
or a recovery code: `:ok` (the secret and recovery codes are deleted), or
`{:error, :invalid_code | :required}`, `:required` when policy requires
one (an administrator's `admin_reset/2` then).

# `enroll_begin`

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

Starts, or resumes, enrollment: `{:ok, %{secret: binary, uri: otpauth}}`
(the URI for a QR code, its label `issuer:username`), or
`{:error, :already_enrolled | :not_found}`. Not yet active: a pending
enrollment changes nothing at sign-in until `enroll_confirm/3`. A
pending secret is kept, so a reload shows the same code.

# `enroll_confirm`

```elixir
@spec enroll_confirm(user(), String.t(), keyword()) ::
  {:ok, [String.t()]}
  | {:error, :invalid_code | :not_started | :already_enrolled}
```

Confirms a pending enrollment with a valid code: `{:ok, recovery_codes}`
(shown once; never retrievable again), or `{:error, :invalid_code |
:not_started | :already_enrolled}`. The confirming code's step is
recorded, so it can't then be used to sign in.

# `normalize`

```elixir
@spec normalize(term()) :: String.t()
```

A submitted code with spaces and dashes removed and letters upper case.

# `recovery_codes_left`

```elixir
@spec recovery_codes_left(user()) :: non_neg_integer()
```

How many unspent recovery codes the user has.

# `regenerate_recovery_codes`

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

Replaces an enrolled user's recovery codes: `{:ok, codes}` (the new
ones, shown once; the old ones no longer work), or
`{:error, :not_enrolled}`.

# `required?`

```elixir
@spec required?(Macaw.Auth.Native.UserRecord.t(), Macaw.Auth.Native.Config.t()) ::
  boolean()
```

Whether policy (`totp_required`) requires a second factor of this user.

# `status`

```elixir
@spec status(Macaw.Auth.Native.UserRecord.t(), Macaw.Auth.Native.Config.t()) ::
  :none | :enrolled | :required
```

The contract's `second_factor` for a row: `:enrolled`, `:required` or `:none`.

# `step`

```elixir
@spec step(integer() | DateTime.t()) :: integer()
```

The step (`div(unix seconds, 30)`) a time falls in.

# `verify`

```elixir
@spec verify(user(), term(), keyword()) :: :ok | {:error, :invalid_code}
```

Checks a TOTP code or an unspent recovery code of an enrolled user. `:ok`
spends it: the step, or the recovery code, never works again. Otherwise
`{:error, :invalid_code}`, the same whatever was wrong (not enrolled, a
wrong code, a replayed one).

---

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