Packages
ash_authentication
5.0.0-rc.7
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/otp/transformer.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.Otp.Transformer do
@moduledoc """
DSL transformer for OTP strategy.
"""
alias Ash.Resource
alias AshAuthentication.Strategy.Otp
alias Spark.Dsl.Transformer
import AshAuthentication.Utils
import AshAuthentication.Validations
import AshAuthentication.Strategy.Custom.Helpers
@doc false
@spec transform(Otp.t(), dsl_state) :: {:ok, Otp.t() | dsl_state} | {:error, any}
when dsl_state: map
def transform(strategy, dsl_state) do
with :ok <-
validate_token_generation_enabled(
dsl_state,
"Token generation must be enabled for the OTP strategy to work."
),
strategy <- maybe_set_sign_in_action_name(strategy),
strategy <- maybe_set_request_action_name(strategy),
strategy <- maybe_set_lookup_action_name(strategy),
strategy <- maybe_set_otp_generator(strategy),
strategy <- maybe_transform_otp_lifetime(strategy),
strategy <- transform_audit_log_window(strategy),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
strategy.lookup_action_name,
&build_lookup_action(&1, strategy)
),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
strategy.sign_in_action_name,
&build_sign_in_action(&1, strategy)
),
{:ok, dsl_state} <-
maybe_build_action(
dsl_state,
strategy.request_action_name,
&build_request_action(&1, strategy)
) do
dsl_state =
dsl_state
|> then(
®ister_strategy_actions(
[
strategy.sign_in_action_name,
strategy.request_action_name,
strategy.lookup_action_name
],
&1,
strategy
)
)
|> put_strategy(strategy)
{:ok, dsl_state}
end
end
defp maybe_transform_otp_lifetime(strategy) when is_integer(strategy.otp_lifetime),
do: %{strategy | otp_lifetime: {strategy.otp_lifetime, :minutes}}
defp maybe_transform_otp_lifetime(strategy), do: strategy
defp transform_audit_log_window(strategy) when is_integer(strategy.audit_log_window),
do: %{strategy | audit_log_window: strategy.audit_log_window * 60}
defp transform_audit_log_window(strategy) when is_tuple(strategy.audit_log_window) do
{value, unit} = strategy.audit_log_window
seconds =
case unit do
:days -> value * 24 * 60 * 60
:hours -> value * 60 * 60
:minutes -> value * 60
:seconds -> value
end
%{strategy | audit_log_window: seconds}
end
# sobelow_skip ["DOS.StringToAtom"]
defp maybe_set_sign_in_action_name(strategy) when is_nil(strategy.sign_in_action_name),
do: %{strategy | sign_in_action_name: String.to_atom("sign_in_with_#{strategy.name}")}
defp maybe_set_sign_in_action_name(strategy), do: strategy
# sobelow_skip ["DOS.StringToAtom"]
defp maybe_set_request_action_name(strategy) when is_nil(strategy.request_action_name),
do: %{strategy | request_action_name: String.to_atom("request_#{strategy.name}")}
defp maybe_set_request_action_name(strategy), do: strategy
# sobelow_skip ["DOS.StringToAtom"]
defp maybe_set_lookup_action_name(strategy) when is_nil(strategy.lookup_action_name),
do: %{strategy | lookup_action_name: String.to_atom("get_by_#{strategy.identity_field}")}
defp maybe_set_lookup_action_name(strategy), do: strategy
defp maybe_set_otp_generator(strategy) when is_nil(strategy.otp_generator),
do: %{strategy | otp_generator: Otp.DefaultGenerator}
defp maybe_set_otp_generator(strategy), do: strategy
defp build_sign_in_action(dsl_state, strategy) do
if strategy.registration_enabled? do
build_sign_in_create_action(dsl_state, strategy)
else
build_sign_in_read_action(dsl_state, strategy)
end
end
defp build_sign_in_create_action(dsl_state, strategy) do
identity_attribute = Resource.Info.attribute(dsl_state, strategy.identity_field)
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: strategy.identity_field,
type: identity_attribute.type,
allow_nil?: false
),
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :argument,
name: strategy.otp_param_name,
type: :string,
allow_nil?: false
)
]
changes = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :change,
change: Otp.SignInChange
)
]
metadata = [
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :metadata,
name: :token,
type: :string,
allow_nil?: false
)
]
identity =
Enum.find(Ash.Resource.Info.identities(dsl_state), fn identity ->
identity.keys == [strategy.identity_field]
end)
Transformer.build_entity(Resource.Dsl, [:actions], :create,
name: strategy.sign_in_action_name,
arguments: arguments,
changes: changes,
metadata: metadata,
upsert?: true,
upsert_identity: identity.name,
upsert_fields: [strategy.identity_field]
)
end
defp build_sign_in_read_action(dsl_state, strategy) do
identity_attribute = Resource.Info.attribute(dsl_state, strategy.identity_field)
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: strategy.identity_field,
type: identity_attribute.type,
allow_nil?: false
),
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: strategy.otp_param_name,
type: :string,
allow_nil?: false
)
]
preparations =
sign_in_brute_force_preparations(strategy, strategy.sign_in_action_name) ++
[
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: Otp.SignInPreparation
)
]
metadata = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :metadata,
name: :token,
type: :string,
allow_nil?: false
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: strategy.sign_in_action_name,
arguments: arguments,
preparations: preparations,
metadata: metadata,
get?: true
)
end
defp build_lookup_action(dsl_state, strategy) do
identity_attribute = Resource.Info.attribute(dsl_state, strategy.identity_field)
field = strategy.identity_field
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument,
name: field,
type: identity_attribute.type,
allow_nil?: false,
description: "The #{field} to look up."
)
]
Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: strategy.lookup_action_name,
arguments: arguments,
get_by: [field],
get?: true,
description: "Look up a user by #{field}."
)
end
defp build_request_action(dsl_state, strategy) do
identity_attribute = Resource.Info.attribute(dsl_state, strategy.identity_field)
arguments = [
Transformer.build_entity!(Resource.Dsl, [:actions, :action], :argument,
name: strategy.identity_field,
type: identity_attribute.type,
allow_nil?: false,
description: "The identity to send a one-time password to."
)
]
preparations = request_brute_force_preparations(strategy, strategy.request_action_name)
touches_resources = brute_force_touches_resources(dsl_state, strategy)
Transformer.build_entity(Resource.Dsl, [:actions], :action,
name: strategy.request_action_name,
arguments: arguments,
run: Otp.Request,
preparations: preparations,
touches_resources: touches_resources,
description: "Send a one-time password to a user if they exist."
)
end
defp sign_in_brute_force_preparations(strategy, action_name) do
case strategy.brute_force_strategy do
{:preparation, preparation} ->
[
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation: preparation
)
]
{:audit_log, _audit_log_name} ->
[
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare,
preparation:
{AshAuthentication.AddOn.AuditLog.BruteForcePreparation, action_name: action_name}
)
]
_ ->
[]
end
end
defp request_brute_force_preparations(strategy, action_name) do
case strategy.brute_force_strategy do
{:preparation, preparation} ->
[
Transformer.build_entity!(Resource.Dsl, [:actions, :action], :prepare,
preparation: preparation
)
]
{:audit_log, _audit_log_name} ->
[
Transformer.build_entity!(Resource.Dsl, [:actions, :action], :prepare,
preparation:
{AshAuthentication.AddOn.AuditLog.IdentityBruteForcePreparation,
action_name: action_name}
)
]
_ ->
[]
end
end
defp brute_force_touches_resources(dsl_state, strategy) do
module = Transformer.get_persisted(dsl_state, :module)
with {:audit_log, audit_log} <- strategy.brute_force_strategy,
{:ok, audit_log} <- AshAuthentication.Info.strategy(dsl_state, audit_log) do
Enum.uniq([module, audit_log.audit_log_resource])
else
_ -> [module]
end
end
end