Packages
ash_authentication
5.0.0-rc.3
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.totp.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.Totp do
use Igniter.Mix.Task
@example "mix ash_authentication.add_strategy.totp --mode 2fa"
@shortdoc "Adds TOTP authentication to your user resource"
@moduledoc """
#{@shortdoc}
TOTP can be used in two modes:
* `2fa` - As a second factor after password authentication (default)
* `primary` - As the primary authentication method (passwordless)
Both modes use an audit log add-on for brute force protection, which will be
created automatically if one doesn't already exist.
## Example
```bash
#{@example}
```
## Options
* `--user`, `-u` - The user resource. Defaults to `YourApp.Accounts.User`
* `--identity-field`, `-i` - The field on the user resource that will be used to identify
the user. Defaults to `email`
* `--mode`, `-m` - Either `primary` or `2fa`. Defaults to `2fa`.
* `--name`, `-n` - The name of the TOTP strategy. Defaults to `totp`.
"""
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :ash,
example: @example,
extra_args?: false,
only: nil,
positional: [],
composes: [
"ash_authentication.add_add_on.audit_log"
],
schema: [
accounts: :string,
user: :string,
identity_field: :string,
mode: :string,
name: :string
],
aliases: [
a: :accounts,
u: :user,
i: :identity_field,
m: :mode,
n: :name
],
defaults: [
identity_field: "email",
mode: "2fa",
name: "totp"
]
}
end
def igniter(igniter) do
options = parse_options(igniter)
if options[:mode] not in [:primary, :"2fa"] do
Mix.shell().error("""
Invalid mode: #{inspect(options[:mode])}
Available modes:
* `2fa` - Use TOTP as a second factor after password authentication
* `primary` - Use TOTP as the primary authentication method
""")
exit({:shutdown, 1})
end
case Igniter.Project.Module.module_exists(igniter, options[:user]) do
{true, igniter} ->
igniter
|> totp(options)
|> AshAuthentication.Igniter.codegen_for_strategy(:totp)
{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(:mode, :"2fa", &String.to_atom/1)
|> Keyword.update(:name, :totp, &String.to_atom/1)
|> Keyword.update!(:accounts, &AshAuthentication.Igniter.maybe_parse_module/1)
|> Keyword.update!(:user, &AshAuthentication.Igniter.maybe_parse_module/1)
end
defp totp(igniter, options) do
igniter
|> Ash.Resource.Igniter.add_new_attribute(options[:user], :totp_secret, """
attribute :totp_secret, :binary do
allow_nil? true
sensitive? true
public? false
end
""")
|> Ash.Resource.Igniter.add_new_attribute(options[:user], :last_totp_at, """
attribute :last_totp_at, :datetime do
allow_nil? true
sensitive? true
public? false
end
""")
|> AshAuthentication.Igniter.ensure_identity(options[:user], options[:identity_field])
|> compose_audit_log(options)
|> add_totp_actions(options)
|> AshAuthentication.Igniter.add_new_strategy(
options[:user],
:totp,
options[:name],
build_strategy_config(options)
)
end
defp add_totp_actions(igniter, options) do
name = options[:name]
setup_name = :"setup_with_#{name}"
confirm_name = :"confirm_setup_with_#{name}"
verify_name = :"verify_with_#{name}"
igniter
|> Ash.Resource.Igniter.add_new_action(options[:user], setup_name, """
update #{inspect(setup_name)} do
require_atomic? false
accept []
change AshAuthentication.Strategy.Totp.GeneratePendingSetupChange
metadata :setup_token, :string, allow_nil?: false
metadata :totp_url, :string, allow_nil?: false
description "Generate a pending TOTP secret and return a setup token. Use the confirm_setup action to activate."
end
""")
|> Ash.Resource.Igniter.add_new_action(options[:user], confirm_name, """
update #{inspect(confirm_name)} do
require_atomic? false
accept []
argument :setup_token, :string do
allow_nil? false
sensitive? true
description "The setup token from the setup action."
end
argument :code, :string do
allow_nil? false
description "The TOTP code to verify."
end
change {AshAuthentication.Strategy.Totp.AuditLogChange,
action_name: #{inspect(confirm_name)}}
change AshAuthentication.Strategy.Totp.ConfirmSetupChange
description "Confirm TOTP setup by verifying a code and activating the secret."
end
""")
|> Ash.Resource.Igniter.add_new_action(options[:user], verify_name, """
action #{inspect(verify_name)} do
argument :user, :struct do
allow_nil? false
sensitive? true
constraints instance_of: __MODULE__
end
argument :code, :string do
allow_nil? false
description "The TOTP code to verify."
end
prepare {AshAuthentication.Strategy.Totp.AuditLogPreparation,
action_name: #{inspect(verify_name)}}
returns :boolean
transaction? true
run AshAuthentication.Strategy.Totp.VerifyAction
description "Is the provided TOTP code valid for the user?"
end
""")
end
defp build_strategy_config(options) do
sign_in_line =
if options[:mode] == :primary do
"\n sign_in_enabled? true"
else
""
end
"""
totp #{inspect(options[:name])} do
identity_field #{inspect(options[:identity_field])}#{sign_in_line}
confirm_setup_enabled? true
brute_force_strategy {:audit_log, :audit_log}
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.Totp do
@shortdoc "Adds TOTP authentication to your user resource"
@moduledoc @shortdoc
use Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'ash_authentication.add_strategy.totp' 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