defmodule AshAuthentication.Strategy.Password.PasswordValidation do
@moduledoc """
A convenience validation that checks that the password argument against the
hashed password stored in the record.
You can use this validation in your changes where you want the user to enter
their current password before being allowed to make a change (eg in a password
change flow).
## Options:
You can provide these options either in the DSL options, or in the changeset
context.
- `strategy_name` - the name of the authentication strategy to use. Required.
- `password_argument` - the name of the argument to check for the current
password. If missing this will default to the `password_field` value
configured on the strategy.
## Examples
```elixir
defmodule MyApp.Accounts.User do
# ...
actions do
update :change_password do
accept []
argument :current_password, :string, sensitive?: true, allow_nil?: false
argument :password, :string, sensitive?: true, allow_nil?: false
argument :password_confirmation, :string, sensitive?: true, allow_nil?: false
validate confirm(:password, :password_confirmation)
validate {AshAuthentication.Strategy.Password.PasswordValidation, strategy_name: :password, password_argument: :current_password}
change {AshAuthentication.Strategy.Password.HashPasswordChange, strategy_name: :password}
end
end
# ...
end
```
"""
use Ash.Resource.Validation
alias Ash.Changeset
alias AshAuthentication.{Errors.AuthenticationFailed, Info}
require Logger
@doc false
@impl true
@spec validate(Changeset.t(), keyword) :: :ok | {:error, Exception.t()}
def validate(changeset, options) do
{:ok, strategy} = get_strategy(changeset, options)
with {:ok, password_arg} <- get_password_arg(changeset, options, strategy),
{:ok, password} <- Changeset.fetch_argument(changeset, password_arg) do
hashed_password = Changeset.get_data(changeset, strategy.hashed_password_field)
if strategy.hash_provider.valid?(password, hashed_password) do
:ok
else
{:error,
AuthenticationFailed.exception(
field: password_arg,
strategy: strategy,
changeset: changeset
)}
end
else
:error ->
strategy.hash_provider.simulate()
{:error,
AuthenticationFailed.exception(
strategy: strategy,
changeset: changeset
)}
end
end
defp get_strategy(changeset, options) do
with :error <- Keyword.fetch(options, :strategy_name),
:error <- Map.fetch(changeset.context, :strategy_name),
:error <- Info.strategy_for_action(changeset.resource, changeset.action) do
Logger.warning(
"[PasswordValidation] Unable to identify the strategy_name for `#{inspect(changeset.action)}` on `#{inspect(changeset.resource)}`."
)
:error
else
{:ok, strategy_name} when is_atom(strategy_name) ->
Info.strategy(changeset.resource, strategy_name)
{:ok, strategy} ->
{:ok, strategy}
end
end
defp get_password_arg(changeset, options, strategy) do
with :error <- Keyword.fetch(options, :password_argument),
:error <- Map.fetch(changeset.context, :password_argument) do
Map.fetch(strategy, :password_field)
end
end
end