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_steptracking (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)
Summary
Functions
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).
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.
Confirms TOTP enrollment by verifying a code against an unconfirmed secret.
Disables MFA for a user after verifying a TOTP or backup code.
Force-disables MFA for a user without code verification (admin action).
Checks if a user has MFA enabled.
Generates TOTP enrollment data.
Regenerates backup codes after verifying a TOTP code ({:totp, code}).
Returns MFA status for a user.
Verifies a TOTP code for an authenticated user.
Verifies a backup code for an authenticated user.
Verifies a TOTP code against a secret with drift and replay prevention.
Functions
@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.
@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.
@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 configuser- 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 verifyopts- Options including:mfa_credential_schemaand: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_schemaconfigured, Sigra may attempt a durablemfa.enroll.failurerow 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.
@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.
@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
@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
@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>>
}}
@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_schemais configured, anmfa.backup_codes_regeneraterow is written viaSigra.Audit.log_multi_safe/3on the sameEcto.Multias 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).
@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}
@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
@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
@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