Packages
ash_authentication
5.0.0-rc.6
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/mix/tasks/ash_authentication.add_strategy.recovery_code.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
# credo:disable-for-this-file Credo.Check.Design.AliasUsage
if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.AshAuthentication.AddStrategy.RecoveryCode do
use Igniter.Mix.Task
@example "mix ash_authentication.add_strategy recovery_code"
@shortdoc "Adds the recovery code authentication strategy"
@moduledoc """
#{@shortdoc}
Creates a recovery code resource and adds the recovery code strategy
to the user resource. Recovery codes are one-time backup codes that
allow users to authenticate when their primary 2FA method (e.g. TOTP)
is unavailable.
## Example
```bash
#{@example}
```
## Options
* `--user`, `-u` - The user resource. Defaults to `YourApp.Accounts.User`
"""
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :ash,
example: @example,
extra_args?: false,
only: nil,
positional: [],
schema: [
accounts: :string,
user: :string,
identity_field: :string
],
aliases: [
a: :accounts,
u: :user,
i: :identity_field
],
defaults: [
identity_field: "email"
]
}
end
def igniter(igniter) do
options = parse_options(igniter)
case Igniter.Project.Module.module_exists(igniter, options[:user]) do
{true, igniter} ->
igniter
|> add_recovery_code_resource(options)
|> AshAuthentication.Igniter.codegen_for_strategy(:recovery_code)
{false, igniter} ->
Igniter.add_issue(igniter, """
User module #{inspect(options[:user])} was not found.
Perhaps you have not yet installed ash_authentication?
""")
end
end
defp parse_options(igniter) do
options =
igniter.args.options
|> Keyword.put_new_lazy(:accounts, fn ->
Igniter.Project.Module.module_name(igniter, "Accounts")
end)
options
|> Keyword.put_new_lazy(:user, fn ->
Module.concat(options[:accounts], User)
end)
|> Keyword.update(:identity_field, :email, &String.to_atom/1)
|> Keyword.update!(:accounts, &maybe_parse_module/1)
|> Keyword.update!(:user, &maybe_parse_module/1)
end
defp maybe_parse_module(value) when is_binary(value), do: Igniter.Project.Module.parse(value)
defp maybe_parse_module(value), do: value
defp add_recovery_code_resource(igniter, options) do
recovery_code_resource =
options[:user]
|> Module.split()
|> :lists.droplast()
|> Enum.concat([RecoveryCode])
|> Module.concat()
{exists?, igniter} = Igniter.Project.Module.module_exists(igniter, recovery_code_resource)
if exists? do
Igniter.add_issue(
igniter,
"""
Recovery code resource already exists: #{inspect(recovery_code_resource)}.
"""
)
else
extensions =
cond do
Code.ensure_loaded?(AshPostgres.DataLayer) -> "postgres"
Code.ensure_loaded?(AshSqlite.DataLayer) -> "sqlite"
true -> ""
end
igniter
|> Igniter.compose_task("ash.gen.resource", [
inspect(recovery_code_resource),
"--uuid-primary-key",
"id",
"--default-actions",
"read,destroy",
"--attribute",
"code:string:required:sensitive",
"--relationship",
"belongs_to:user:#{inspect(options[:user])}:required",
"--extend",
extensions
])
|> Ash.Resource.Igniter.add_new_action(recovery_code_resource, :create, """
create :create do
primary? true
accept [:code]
end
""")
|> Ash.Resource.Igniter.add_new_relationship(
options[:user],
:recovery_codes,
"""
has_many :recovery_codes, #{inspect(recovery_code_resource)}
"""
)
|> compose_audit_log(options)
|> Ash.Resource.Igniter.add_new_action(options[:user], :verify_with_recovery_code, """
action :verify_with_recovery_code do
argument :user, :struct do
allow_nil? false
sensitive? true
constraints instance_of: __MODULE__
end
argument :code, :string do
allow_nil? false
sensitive? true
end
prepare {AshAuthentication.AddOn.AuditLog.BruteForcePreparation,
action_name: :verify_with_recovery_code}
returns :term
transaction? true
run AshAuthentication.Strategy.RecoveryCode.VerifyAction
touches_resources [#{inspect(recovery_code_resource)}]
description "Verify a recovery code and return the user if valid, nil otherwise."
end
""")
|> Ash.Resource.Igniter.add_new_action(options[:user], :generate_recovery_code_codes, """
update :generate_recovery_code_codes do
require_atomic? false
accept []
argument :recovery_codes, {:array, :string} do
allow_nil? false
sensitive? true
default {AshAuthentication.Strategy.RecoveryCode.Actions, :generate_codes_list,
[12, 10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"]}
end
change {Ash.Resource.Change.CascadeDestroy, relationship: :recovery_codes, after_action?: false}
change {AshAuthentication.Strategy.RecoveryCode.HashRecoveryCodesChange, hash_provider: AshAuthentication.SHA256Provider}
change {Ash.Resource.Change.ManageRelationship,
argument: :recovery_codes,
relationship: :recovery_codes,
opts: [type: :create, value_is_key: :code]}
metadata :recovery_codes, {:array, :string}, allow_nil?: false
touches_resources [#{inspect(recovery_code_resource)}]
description "Generate new recovery codes for the user, replacing any existing codes."
end
""")
|> AshAuthentication.Igniter.add_new_strategy(
options[:user],
:recovery_code,
:recovery_code,
"""
recovery_code do
recovery_code_resource #{inspect(recovery_code_resource)}
brute_force_strategy {:audit_log, :audit_log}
end
"""
)
end
end
defp compose_audit_log(igniter, options) do
{igniter, has_audit_log?} =
AshAuthentication.Igniter.defines_add_on(igniter, options[:user], :audit_log, nil)
if has_audit_log? do
igniter
else
Igniter.compose_task(
igniter,
"ash_authentication.add_add_on.audit_log",
["--user", inspect(options[:user])]
)
end
end
end
else
defmodule Mix.Tasks.AshAuthentication.AddStrategy.RecoveryCode do
@shortdoc "Adds the recovery code authentication strategy"
@moduledoc @shortdoc
use Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'ash_authentication.add_strategy.recovery_code' requires igniter to be run.
Please install igniter and try again.
For more information, see: https://hexdocs.pm/igniter
""")
exit({:shutdown, 1})
end
end
end