Packages
ash_authentication
5.0.0-rc.12
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.
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/strategies/otp/request.ex
# SPDX-FileCopyrightText: 2026 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.Otp.Request do
@moduledoc """
Implementation of the OTP request action.
Looks up the user via the configured `lookup_action_name`, generates an OTP
code, stores an internal JWT keyed by a deterministic JTI, and dispatches the
code via the configured sender. Always returns `:ok` so user enumeration is
not possible from the response. The audit-log status override is set so that
failed lookups (and senders) are recorded as `:failure`.
"""
use Ash.Resource.Actions.Implementation
alias Ash.{ActionInput, Query}
alias AshAuthentication.{AddOn.AuditLog.Auditor, Errors.SenderFailed, Info, Strategy.Otp}
require Ash.Query
import Ash.Expr
@doc false
@impl true
def run(input, _opts, context) do
strategy = Info.strategy_for_action!(input.resource, input.action.name)
identity = ActionInput.get_argument(input, strategy.identity_field)
context_opts = Ash.Context.to_opts(context)
input.resource
|> Query.new()
|> Query.set_context(%{private: %{ash_authentication?: true}})
|> Query.for_read(
strategy.lookup_action_name,
%{strategy.identity_field => identity},
context_opts
)
|> Query.filter(^ref(strategy.identity_field) == ^identity)
|> Ash.read_one()
|> case do
{:error, error} ->
{:error, error}
{:ok, nil} ->
handle_unknown_identity(input, strategy, identity, context_opts, context)
{:ok, user} ->
handle_known_user(input, strategy, user, context_opts, context)
end
end
defp handle_known_user(input, strategy, user, context_opts, context) do
otp_code = generate_code(strategy)
token_result =
if strategy.registration_enabled? do
identity = Map.get(user, strategy.identity_field)
Otp.generate_otp_token_for_identity(
strategy,
identity,
otp_code,
context_opts,
context
)
else
Otp.generate_otp_token_for(strategy, user, otp_code, context_opts, context)
end
case token_result do
{:ok, _token} ->
deliver(input, strategy, user, otp_code, context)
_ ->
Auditor.record_status_override(input, :failure)
:ok
end
end
defp handle_unknown_identity(
input,
%{registration_enabled?: true} = strategy,
identity,
context_opts,
context
)
when not is_nil(identity) do
otp_code = generate_code(strategy)
case Otp.generate_otp_token_for_identity(
strategy,
identity,
otp_code,
context_opts,
context
) do
{:ok, _token} ->
deliver(input, strategy, to_string(identity), otp_code, context)
_ ->
Auditor.record_status_override(input, :failure)
:ok
end
end
defp handle_unknown_identity(input, _strategy, _identity, _context_opts, _context) do
Auditor.record_status_override(input, :failure)
:ok
end
defp deliver(input, strategy, recipient, otp_code, context) do
{sender, send_opts} = strategy.sender
build_opts =
Keyword.merge(send_opts,
tenant: context.tenant,
source_context: context.source_context
)
case sender.send(recipient, otp_code, build_opts) do
{:error, reason} when not is_struct(reason) ->
Auditor.record_status_override(input, :failure)
{:error, SenderFailed.exception(sender: sender, reason: reason, strategy: strategy.name)}
{:error, _} = error ->
Auditor.record_status_override(input, :failure)
error
_ ->
Auditor.record_status_override(input, :success)
:ok
end
end
defp generate_code(strategy) do
generator = strategy.otp_generator || Otp.DefaultGenerator
generator.generate(
length: strategy.otp_length,
characters: strategy.otp_characters
)
end
end