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/recovery_code/actions.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.RecoveryCode.Actions do
@moduledoc """
Actions for the recovery code strategy.
Provides the code interface for recovery code verification and generation.
"""
alias Ash.{ActionInput, Changeset}
alias AshAuthentication.{Errors, Info, Strategy.RecoveryCode}
@doc """
Verify a recovery code for a user.
Takes a user and a recovery code, verifies against stored hashed codes,
deletes the code on success, and returns the authenticated user.
"""
@spec verify(RecoveryCode.t(), map, keyword) ::
{:ok, Ash.Resource.Record.t()} | {:error, any}
def verify(strategy, params, options) do
options =
options
|> Keyword.put_new_lazy(:domain, fn -> Info.domain!(strategy.resource) end)
strategy.resource
|> ActionInput.new()
|> ActionInput.set_context(%{private: %{ash_authentication?: true}})
|> ActionInput.for_action(strategy.verify_action_name, params, options)
|> Ash.run_action()
|> case do
{:ok, user} when not is_nil(user) ->
{:ok, user}
{:ok, nil} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :verify,
message: "Invalid recovery code"
}
)}
{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}
end
end
@doc """
Generate new recovery codes for a user.
Deletes any existing recovery codes and generates new ones.
Returns the user with plaintext codes in `__metadata__.recovery_codes`.
"""
@spec generate(RecoveryCode.t(), map, keyword) ::
{:ok, Ash.Resource.Record.t()} | {:error, any}
def generate(strategy, params, options) do
user = Map.get(params, :user) || Map.get(params, "user")
options =
options
|> Keyword.put_new_lazy(:domain, fn -> Info.domain!(strategy.resource) end)
user
|> Changeset.new()
|> Changeset.set_context(%{private: %{ash_authentication?: true}})
|> Changeset.for_update(strategy.generate_action_name, %{}, options)
|> Ash.update()
|> case do
{:ok, user} ->
{:ok, user}
{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}
end
end
@doc """
Generate a list of random recovery codes using a CSPRNG.
"""
@spec generate_codes_list(pos_integer, pos_integer, String.t()) :: [String.t()]
def generate_codes_list(length, count, alphabet) do
Enum.map(1..count, fn _ -> generate_code(length, alphabet) end)
end
@doc """
Generate a single random recovery code using `:crypto.strong_rand_bytes/1`.
"""
@spec generate_code(pos_integer, String.t()) :: String.t()
def generate_code(length, alphabet) do
alphabet_list = String.graphemes(alphabet)
alphabet_size = length(alphabet_list)
bytes_needed = max(1, ceil(:math.log2(alphabet_size) / 8))
Enum.map_join(1..length, fn _ ->
random_int =
bytes_needed
|> :crypto.strong_rand_bytes()
|> :binary.decode_unsigned()
Enum.at(alphabet_list, rem(random_int, alphabet_size))
end)
end
end