Packages
ash_authentication
5.0.0-rc.6
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/strategies/totp/audit_log_change.ex
# SPDX-FileCopyrightText: 2025 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.Totp.AuditLogChange do
@moduledoc """
Change that checks the audit log for failed TOTP attempts before update actions.
This is the change variant of `AuditLogPreparation` for use with update actions
like `confirm_setup`.
When `brute_force_strategy: {:audit_log, :my_audit_log}` is configured,
this change queries the audit log for failed TOTP attempts within
a time window. If the number of failures exceeds the configured maximum,
the request is denied with an `AuthenticationFailed` error.
The window and max failures are configured via DSL options:
- `audit_log_window` - time window for counting failures (default: 5 minutes)
- `audit_log_max_failures` - maximum allowed failures before blocking (default: 5)
Failures are counted across ALL TOTP actions (sign_in, verify, confirm_setup)
for the same user, not per-action.
"""
use Ash.Resource.Change
alias Ash.Changeset
alias AshAuthentication.{Errors.AuthenticationFailed, Info}
alias AshAuthentication.Strategy.Totp.{AuditLogHelpers, Helpers}
@impl true
def init(opts), do: {:ok, opts}
@impl true
def change(changeset, opts, context) do
with {:ok, strategy} <- Info.find_strategy(changeset, context, opts),
{:ok, audit_log} <- get_audit_log(changeset.resource, strategy),
user when not is_nil(user) <- changeset.data do
Changeset.before_action(changeset, &apply_rate_limit(&1, user, strategy, audit_log, opts))
else
_ -> changeset
end
end
defp apply_rate_limit(changeset, user, strategy, audit_log, opts) do
case check_rate_limit(user, strategy, audit_log, opts) do
:ok -> changeset
{:error, error} -> Changeset.add_error(changeset, error)
end
end
defp get_audit_log(resource, strategy) do
case strategy.brute_force_strategy do
{:audit_log, audit_log_name} ->
Info.strategy(resource, audit_log_name)
_ ->
:error
end
end
defp check_rate_limit(user, strategy, audit_log, opts) do
subject = AshAuthentication.user_to_subject(user)
window = Helpers.time_to_seconds(strategy.audit_log_window)
max_failures = strategy.audit_log_max_failures
cutoff = DateTime.add(DateTime.utc_now(), -window, :second)
case AuditLogHelpers.count_failures(audit_log, subject, cutoff) do
{:ok, failure_count} when failure_count >= max_failures ->
action_name = Keyword.get(opts, :action_name, :unknown)
{:error,
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: action_name,
message: "Too many failed TOTP attempts"
}
)}
{:ok, _failure_count} ->
:ok
{:error, reason} ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: Keyword.get(opts, :action_name, :unknown),
message: "Audit log unavailable",
reason: reason
}
)}
end
end
end