Sigra.Lockout (Sigra v0.2.3)

Copy Markdown View Source

Account lockout logic for brute force prevention.

Checks and manages lockout state based on failed login attempts. The lockout counter tracks failed password attempts only. After reaching the threshold, the account is temporarily locked for a configurable duration and auto-unlocks when the duration expires.

Security Properties

  • Lockout check happens before password hash verification (saves CPU).
  • Messages are enumeration-safe (generic text via Sigra.Error.safe_message/1).
  • Counter resets on successful login.
  • Auto-unlocks after duration expires.
  • Hooks via telemetry only ([:sigra, :security, :lockout]).

Options

All functions accept the following options:

  • :threshold - Number of failed attempts before lockout. Default: 5.
  • :duration - Lockout duration in seconds. Default: 900 (15 minutes).

Examples

case Sigra.Lockout.check(user) do
  :ok -> # proceed to password verification
  {:error, :account_locked, remaining} -> # account is locked
end

Summary

Functions

Record a lockout event in the audit log (standalone, D-28).

Check if user is locked out.

Increment failed login attempts. Sets locked_at when threshold reached.

Return lock status with remaining seconds.

Check if user is currently locked.

Reset lockout state after successful login.

Functions

audit_lockout(opts)

(since 0.9.0)
@spec audit_lockout(keyword()) :: :ok

Record a lockout event in the audit log (standalone, D-28).

Invoked by Sigra.Auth after a lockout is triggered. Exposed as a no-op-safe helper so callers don't need to build Sigra.Audit opts manually. Uses Sigra.Audit.log_safe which skips when audit_schema is nil.

check(user, opts \\ [])

(since 0.4.0)
@spec check(
  struct() | nil,
  keyword()
) :: :ok | {:error, :account_locked, non_neg_integer()}

Check if user is locked out.

Returns :ok or {:error, :account_locked, remaining_seconds}.

Returns :ok for nil users (non-existent account) to support enumeration-safe flows where a dummy hash is performed regardless.

Examples

iex> Sigra.Lockout.check(nil)
:ok

iex> Sigra.Lockout.check(%{failed_login_attempts: 3, locked_at: nil})
:ok

increment!(repo, user, opts \\ [])

(since 0.4.0)
@spec increment!(module(), struct(), keyword()) :: struct()

Increment failed login attempts. Sets locked_at when threshold reached.

Must be called within a transaction or after failed password verification. Only sets locked_at when failed_login_attempts reaches the threshold, not before.

Examples

updated_user = Sigra.Lockout.increment!(MyApp.Repo, user)

lock_status(user, opts \\ [])

(since 0.4.0)
@spec lock_status(
  struct() | nil,
  keyword()
) :: :unlocked | {:locked, non_neg_integer()}

Return lock status with remaining seconds.

Returns {:locked, remaining_seconds} or :unlocked. Returns :unlocked for nil users.

Examples

case Sigra.Lockout.lock_status(user) do
  :unlocked -> # user can log in
  {:locked, seconds} -> # locked for N more seconds
end

locked?(user, opts \\ [])

(since 0.4.0)
@spec locked?(
  struct() | nil,
  keyword()
) :: boolean()

Check if user is currently locked.

Returns true when within lockout window, false otherwise. Returns false for nil users.

Examples

iex> Sigra.Lockout.locked?(nil)
false

reset!(repo, user)

(since 0.4.0)
@spec reset!(
  module(),
  struct()
) :: struct()

Reset lockout state after successful login.

Sets failed_login_attempts to 0 and locked_at to nil.

Examples

updated_user = Sigra.Lockout.reset!(MyApp.Repo, user)