# `Sigra.MFA`
[🔗](https://github.com/sztheory/sigra/blob/v1.20.0/lib/sigra/mfa.ex#L1)

Core MFA orchestrator module.

All security-critical MFA operations live here. The generated
`MyApp.Auth` context delegates to these functions for TOTP enrollment,
verification, backup code management, and MFA lifecycle.

## Usage

    # Enrollment
    {:ok, enrollment} = Sigra.MFA.enroll(config, account: "user@example.com")

    # Verification
    {:ok, :verified} = Sigra.MFA.verify(config, user, "123456")

    # Status check
    Sigra.MFA.enabled?(config, user)

## Security Properties

- TOTP secrets generated via NimbleTOTP (RFC 6238 compliant)
- Drift window: configurable +/- steps (default +/-1 = 30s each side)
- Replay prevention via `last_verified_step` tracking (D-41)
- Backup codes: SHA-256 hashed, atomic consumption (D-13, D-16)
- Lockout after configurable failed attempts (D-19)
- TOTP secrets encrypted at rest via cloak_ecto (D-09)

# `audit_backup_codes_regenerate`

```elixir
@spec audit_backup_codes_regenerate(Sigra.Config.t(), struct(), non_neg_integer()) ::
  :ok
```

Record an `mfa.backup_codes_regenerate` audit row via `Ecto.Multi` +
`Sigra.Audit.log_multi_safe/3` inside `Repo.transaction/1` when `:audit_schema`
is configured (same telemetry semantics as legacy `log_safe/3` on failure).

This is **not** the authoritative audit path when audit is enabled for
library-driven rotation — use `regenerate_backup_codes/4`, which appends
the same action to the rotation `Ecto.Multi`. This function remains for
ad-hoc or legacy call sites.

# `audit_trust_browser`

```elixir
@spec audit_trust_browser(
  Sigra.Config.t(),
  struct()
) :: :ok
```

Record an `mfa.trust_browser` audit row via `Ecto.Multi` +
`Sigra.Audit.log_multi_safe/3` inside `Repo.transaction/1` when `:audit_schema`
is configured. Intended for `Sigra.MFA.Trust` when a browser is marked trusted.

# `confirm_enrollment`
*since 0.6.0* 

```elixir
@spec confirm_enrollment(Sigra.Config.t(), struct(), binary(), String.t(), keyword()) ::
  {:ok, map()} | {:error, :invalid_code}
```

Confirms TOTP enrollment by verifying a code against an unconfirmed secret.

If valid, creates the MFA credential in the database with the encrypted secret,
generates backup codes, and returns both.

## Parameters

- `config` - Sigra config
- `user` - The user struct (must have `:id`)
- `raw_secret` - The raw TOTP secret binary (from enrollment, held in session)
- `code` - The 6-digit TOTP code to verify
- `opts` - Options including `:mfa_credential_schema` and `:backup_code_schema`

## Returns

- `{:ok, %{credential: credential, backup_codes: codes}}` when the code is valid
  and the enrollment transaction succeeds.
- `{:error, :invalid_code}` when the TOTP check fails. This tuple is returned
  regardless of audit subsystem outcome: with `:audit_schema` configured, Sigra
  may attempt a durable `mfa.enroll.failure` row in an audit-only transaction
  (`Repo.transaction/1` + `Multi` + `log_multi_safe`); audit persistence failure
  does not change the return value. Operators should monitor
  `[:sigra, :audit, :log_safe_error]` for forensic signals.

# `disable`
*since 0.6.0* 

```elixir
@spec disable(Sigra.Config.t(), struct(), String.t(), keyword()) ::
  {:ok, :disabled} | {:error, atom()}
```

Disables MFA for a user after verifying a TOTP or backup code.

Requires current code verification before deletion (D-59).
Deletes MFA credential, all backup codes, and increments trust_epoch (D-60).

## Options

- `:mfa_credential_schema` - The generated MFA credential Ecto schema module
- `:backup_code_schema` - The generated backup code Ecto schema module

When `:audit_schema` is set, a failed cleanup transaction returns
`{:error, :mfa_audit_failed}` (audit insert changeset) or
`{:error, :mfa_disable_failed}` (other `Ecto.Multi` steps). Database constraint
violations on the audit row may still raise `Ecto.ConstraintError` (same as
other audited MFA transactions) after the repo rolls back.

# `disable!`
*since 0.6.0* 

```elixir
@spec disable!(Sigra.Config.t(), struct(), keyword()) :: {:ok, :disabled}
```

Force-disables MFA for a user without code verification (admin action).

Same cleanup as `disable/4` but skips code verification (D-65).

Raises `RuntimeError` when the cleanup `Ecto.Multi` returns an error tuple
(after rollback). Database constraint violations on the audit insert may
instead raise `Ecto.ConstraintError`, matching other audited MFA paths.

## Options

- `:mfa_credential_schema` - The generated MFA credential Ecto schema module
- `:backup_code_schema` - The generated backup code Ecto schema module

# `enabled?`
*since 0.6.0* 

```elixir
@spec enabled?(
  Sigra.Config.t(),
  struct()
) :: boolean()
```

Checks if a user has MFA enabled.

Returns `true` if the user has an MFA credential with `enabled_at != nil`.

## Options

- `:mfa_credential_schema` - The generated MFA credential Ecto schema module

# `enroll`
*since 0.6.0* 

```elixir
@spec enroll(
  Sigra.Config.t(),
  keyword()
) :: {:ok, map()}
```

Generates TOTP enrollment data.

Creates a new TOTP secret and returns the base32-encoded secret,
otpauth URI, optional SVG QR code, and raw binary secret.

The raw secret should be held in the encrypted session until the user
confirms enrollment with a valid code (D-03).

## Options

- `:account` - The account name for the otpauth URI (e.g., user email)

## Returns

    {:ok, %{
      secret: "BASE32ENCODED...",
      otpauth_uri: "otpauth://totp/...",
      svg: "<svg>...</svg>" | nil,
      raw_secret: <<binary>>
    }}

# `regenerate_backup_codes`
*since 0.6.0* 

```elixir
@spec regenerate_backup_codes(
  Sigra.Config.t(),
  struct(),
  {:totp, String.t()},
  keyword()
) ::
  {:ok, %{backup_codes: [String.t()]}}
  | {:error, :invalid_code, non_neg_integer()}
  | {:error, :lockout, non_neg_integer()}
  | {:error, :not_enrolled}
```

Regenerates backup codes after verifying a TOTP code (`{:totp, code}`).

Backup codes **cannot** authorize rotation — only a valid TOTP proof is
accepted.

Replacement runs in a single `Repo.transaction/1` (`delete_all` + `insert_all`
+ optional audit). When `:audit_schema` is configured, an
`mfa.backup_codes_regenerate` row is written via `Sigra.Audit.log_multi_safe/3`
on the same `Ecto.Multi` as the replace (atomic with persistence).

On success, lockout counters are cleared and `last_verified_step` /
`last_used_at` are updated like `verify/4`, but this path intentionally
**does not** emit `mfa.verify.success` — rotation is covered by
`mfa.backup_codes_regenerate` when audit is enabled.

## Options

- `:mfa_credential_schema` (required)
- `:backup_code_schema` (required)

## Returns

Same error shapes as `verify/4` for `:not_enrolled`, `:lockout`, and
`:invalid_code` (with remaining attempts).

# `status`
*since 0.6.0* 

```elixir
@spec status(Sigra.Config.t(), struct(), keyword()) :: map()
```

Returns MFA status for a user.

## Options

- `:mfa_credential_schema` - The generated MFA credential Ecto schema module (via config or opts)
- `:backup_code_schema` - The generated backup code Ecto schema module (via config or opts)

## Returns

    %{enabled: boolean, type: "totp" | nil, backup_codes_remaining: integer}

# `verify`
*since 0.6.0* 

```elixir
@spec verify(Sigra.Config.t(), struct(), String.t(), keyword()) ::
  {:ok, :verified}
  | {:error, :invalid_code, non_neg_integer()}
  | {:error, :lockout, non_neg_integer()}
  | {:error, :not_enrolled}
```

Verifies a TOTP code for an authenticated user.

Fetches the MFA credential, checks lockout, verifies the code with
drift and replay prevention. On success, resets attempt counter and
updates `last_verified_step`.

## Options

- `:mfa_credential_schema` - The generated MFA credential Ecto schema module

## Returns

- `{:ok, :verified}` on success
- `{:error, :invalid_code, remaining_attempts}` on wrong code
- `{:error, :lockout, remaining_seconds}` when locked out
- `{:error, :not_enrolled}` when user has no MFA credential

# `verify_backup`
*since 0.6.0* 

```elixir
@spec verify_backup(Sigra.Config.t(), struct(), String.t(), keyword()) ::
  {:ok, :consumed, non_neg_integer()}
  | {:error, :invalid_backup_code, non_neg_integer()}
  | {:error, :lockout, non_neg_integer()}
  | {:error, :not_enrolled}
```

Verifies a backup code for an authenticated user.

Fetches MFA credential, checks lockout (shared counter per D-19),
calls `BackupCodes.consume/4`. On success, resets attempt counter.

## Options

- `:mfa_credential_schema` - The generated MFA credential Ecto schema module
- `:backup_code_schema` - The generated backup code Ecto schema module

# `verify_totp`
*since 0.6.0* 

```elixir
@spec verify_totp(binary(), String.t(), integer(), non_neg_integer()) ::
  {:ok, integer()} | {:error, :replay | :invalid_code}
```

Verifies a TOTP code against a secret with drift and replay prevention.

This is exposed as a public function for testing purposes but is intended
as an internal helper. Checks the code against the current time step and
+/- drift steps. Rejects codes from steps at or below `last_verified_step`
to prevent replay attacks (D-41).

## Returns

- `{:ok, step}` if valid (step is the TOTP time step that matched)
- `{:error, :replay}` if code matches but step <= last_verified_step
- `{:error, :invalid_code}` if code doesn't match any step

---

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