Packages
ash_authentication
5.0.0-rc.5
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/recovery_code/verify_action.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.RecoveryCode.VerifyAction do
@moduledoc """
Implementation of the recovery code verify action.
Checks if a provided recovery code matches any of the user's stored hashed
codes. On success, the matched code is deleted (single-use).
Uses two verification strategies depending on the hash provider:
- **Deterministic** (e.g. SHA-256): hashes the input once and performs an
atomic database filter+delete via `Ash.bulk_destroy`. Inherently race-safe.
- **Non-deterministic** (e.g. bcrypt): loads codes with `FOR UPDATE` locking,
iterates with constant-time padding, then deletes by ID.
"""
use Ash.Resource.Actions.Implementation
require Ash.Query
import Ash.Expr, only: [ref: 1]
import AshAuthentication.Utils, only: [maybe_lock: 2]
alias Ash.ActionInput
alias AshAuthentication.Info
@doc false
@impl true
def run(input, _opts, context) do
user = ActionInput.get_argument(input, :user)
code = ActionInput.get_argument(input, :code)
opts = context |> Ash.Context.to_opts()
ash_context = input.context
{:ok, strategy} = Info.strategy_for_action(input.resource, input.action.name)
if strategy.hash_provider.deterministic?() do
verify_deterministic(user, code, strategy, ash_context, opts)
else
verify_non_deterministic(user, code, strategy, opts)
end
end
# sobelow_skip ["DOS.BinToAtom"]
defp verify_deterministic(user, code, strategy, ash_context, opts) do
code_field = strategy.code_field
user_id_field = :"#{strategy.user_relationship_name}_id"
case AshAuthentication.HashProvider.call_hash(strategy.hash_provider, code, ash_context) do
{:ok, hashed_input} ->
domain = Keyword.get_lazy(opts, :domain, fn -> Info.domain!(strategy.resource) end)
result =
strategy.recovery_code_resource
|> Ash.Query.filter(
^ref(user_id_field) == ^user.id and ^ref(code_field) == ^hashed_input
)
|> Ash.Query.limit(1)
|> Ash.Query.set_context(%{private: %{ash_authentication?: true}})
|> Ash.bulk_destroy(:destroy, %{},
strategy: [:atomic, :atomic_batches, :stream],
return_records?: true,
context: %{private: %{ash_authentication?: true}},
domain: domain
)
case result do
%{status: :success, records: [_]} ->
{:ok, user}
%{status: :success, records: []} ->
{:ok, nil}
%{errors: errors} ->
{:error, Ash.Error.to_ash_error(errors)}
end
:error ->
strategy.hash_provider.simulate()
{:ok, nil}
end
end
# sobelow_skip ["DOS.BinToAtom"]
defp verify_non_deterministic(user, code, strategy, opts) do
code_field = strategy.code_field
user_id_field = :"#{strategy.user_relationship_name}_id"
domain = Keyword.get_lazy(opts, :domain, fn -> Info.domain!(strategy.resource) end)
codes =
strategy.recovery_code_resource
|> Ash.Query.filter(^ref(user_id_field) == ^user.id)
|> Ash.Query.set_context(%{private: %{ash_authentication?: true}})
|> maybe_lock(:for_update)
|> Ash.read!(Keyword.merge(opts, domain: domain))
matched = find_matching_code(codes, code, code_field, strategy)
pad_count = max(0, strategy.recovery_code_count - length(codes))
Enum.each(1..max(pad_count, 1)//1, fn _ -> strategy.hash_provider.simulate() end)
if is_nil(matched) do
{:ok, nil}
else
[pk_field] = Ash.Resource.Info.primary_key(strategy.recovery_code_resource)
result =
strategy.recovery_code_resource
|> Ash.Query.filter(^ref(pk_field) == ^Map.get(matched, pk_field))
|> Ash.Query.set_context(%{private: %{ash_authentication?: true}})
|> Ash.bulk_destroy(:destroy, %{},
strategy: [:atomic, :atomic_batches, :stream],
return_records?: true,
context: %{private: %{ash_authentication?: true}},
domain: domain
)
case result do
%{status: :success, records: [_]} ->
{:ok, user}
%{status: :success, records: []} ->
{:ok, nil}
%{errors: errors} ->
{:error, Ash.Error.to_ash_error(errors)}
end
end
end
defp find_matching_code(codes, provided_code, code_field, strategy) do
Enum.find(codes, fn code_record ->
stored_hash = Map.get(code_record, code_field)
strategy.hash_provider.valid?(provided_code, stored_hash)
end)
end
end