Packages
ash_authentication
5.0.0-rc.11
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/sign_in_helpers.ex
# SPDX-FileCopyrightText: 2026 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.Otp.SignInHelpers do
@moduledoc false
alias AshAuthentication.TokenResource
alias AshAuthentication.TokenResource.Info, as: TokenInfo
import AshAuthentication.Utils, only: [maybe_lock: 2]
@doc """
Fetch a stored OTP token by JTI while holding a `SELECT FOR UPDATE` row lock.
Both sign-in paths (the read-backed preparation and the create-backed change)
use this to serialize concurrent sign-in attempts for the same OTP code. The
caller is expected to invoke this inside a transaction.
"""
@spec get_otp_token_locked(module, String.t(), keyword) ::
{:ok, [Ash.Resource.record()]} | {:error, any}
def get_otp_token_locked(token_resource, jti, context_opts) do
with {:ok, domain} <- TokenInfo.token_domain(token_resource),
{:ok, get_token_action_name} <- TokenInfo.token_get_token_action_name(token_resource) do
token_resource
|> Ash.Query.new()
|> Ash.Query.set_context(%{private: %{ash_authentication?: true}})
|> maybe_lock(:for_update)
|> Ash.Query.for_read(
get_token_action_name,
%{"jti" => jti, "purpose" => "otp"},
Keyword.take(
Keyword.put(context_opts, :domain, domain),
[:actor, :authorize?, :tenant, :tracer, :domain]
)
)
|> Ash.read()
end
end
@doc """
Consume an OTP token that has been located via `get_otp_token_locked/3`.
Returns `:ok` if the token was unrevoked (and has now been revoked), or if
the strategy is not using single-use tokens. Returns
`{:error, :already_consumed}` if a revocation record already exists — which
means another concurrent request won the race after the row lock was released.
"""
@spec consume_token(struct, module, String.t(), String.t(), keyword) ::
:ok | {:error, :already_consumed}
def consume_token(%{single_use_token?: false}, _token_resource, _jti, _subject, _context_opts),
do: :ok
def consume_token(_strategy, token_resource, jti, subject, context_opts) do
if TokenResource.Actions.jti_revoked?(token_resource, jti, context_opts) do
{:error, :already_consumed}
else
TokenResource.Actions.revoke_jti(token_resource, jti, subject, context_opts)
:ok
end
end
end