Packages
ash_authentication
5.0.0-rc.11
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/mix/tasks/ash_authentication.add_strategy.otp.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.Otp do
use Igniter.Mix.Task
@example "mix ash_authentication.add_strategy.otp"
@shortdoc "Adds one-time password (OTP) authentication to your user resource"
@moduledoc """
#{@shortdoc}
Adds the OTP strategy to your user resource — passwordless authentication
via a short code (e.g. `XKPTMH`) sent via email or SMS.
Brute-force protection is required by the strategy. By default this task
composes an `audit_log` add-on (creating one if it doesn't already exist)
and wires `brute_force_strategy {:audit_log, :audit_log}` into the
strategy.
## 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`
* `--name`, `-n` - The name of the OTP strategy. Defaults to `otp`.
## Notes
`registration_enabled?: true` (sign-in becomes an upsert) is not currently
supported by this task because the verifier forbids the `:audit_log`
brute-force strategy in registration mode, and the alternatives
(`:rate_limit` or `{:preparation, ...}`) require user-supplied
configuration. Add the strategy by hand and follow the OTP tutorial if you
need that mode.
"""
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,
name: :string,
registration: :boolean
],
aliases: [
a: :accounts,
u: :user,
i: :identity_field,
n: :name
],
defaults: [
identity_field: "email",
name: "otp"
]
}
end
def igniter(igniter) do
options = parse_options(igniter)
if options[:registration] do
Mix.shell().error("""
`--registration` is not supported by this task.
When `registration_enabled?` is true, the OTP strategy cannot use the
`:audit_log` brute-force strategy (the verifier rejects it). The
alternatives are `:rate_limit` (requires the AshRateLimiter extension
and a backend) or `{:preparation, MyMod}` (requires you to write a
preparation module). Both need decisions this generator can't make
safely on your behalf.
Add the OTP strategy by hand and follow the OTP tutorial:
https://hexdocs.pm/ash_authentication/otp.html
""")
exit({:shutdown, 1})
end
case Igniter.Project.Module.module_exists(igniter, options[:user]) do
{true, igniter} ->
igniter
|> otp(options)
|> AshAuthentication.Igniter.codegen_for_strategy(:otp)
{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(:name, :otp, &String.to_atom/1)
|> Keyword.update!(:accounts, &AshAuthentication.Igniter.maybe_parse_module/1)
|> Keyword.update!(:user, &AshAuthentication.Igniter.maybe_parse_module/1)
end
# sobelow_skip ["DOS.BinToAtom"]
defp otp(igniter, options) do
sender = Module.concat(options[:user], Senders.SendOtp)
strategy_name = options[:name]
identity_field = options[:identity_field]
request_action = :"request_#{strategy_name}"
sign_in_action = :"sign_in_with_#{strategy_name}"
igniter
|> Ash.Resource.Igniter.add_new_attribute(options[:user], identity_field, """
attribute #{inspect(identity_field)}, :ci_string do
allow_nil? false
public? true
end
""")
|> AshAuthentication.Igniter.ensure_identity(options[:user], identity_field)
|> AshAuthentication.Igniter.ensure_get_by_action(options[:user], identity_field)
|> compose_audit_log(options)
|> Ash.Resource.Igniter.add_new_action(options[:user], request_action, """
action #{inspect(request_action)} do
argument #{inspect(identity_field)}, :ci_string do
allow_nil? false
description "The identity to send a one-time password to."
end
run AshAuthentication.Strategy.Otp.Request
description "Send a one-time password to a user if they exist."
end
""")
|> Ash.Resource.Igniter.add_new_action(options[:user], sign_in_action, """
read #{inspect(sign_in_action)} do
description "Sign in a user with a one-time password."
get? true
argument #{inspect(identity_field)}, :ci_string do
allow_nil? false
end
argument :otp, :string do
allow_nil? false
sensitive? true
end
prepare AshAuthentication.Strategy.Otp.SignInPreparation
metadata :token, :string do
allow_nil? false
end
end
""")
|> AshAuthentication.Igniter.add_new_strategy(
options[:user],
:otp,
strategy_name,
"""
otp #{inspect(strategy_name)} do
identity_field #{inspect(identity_field)}
brute_force_strategy {:audit_log, :audit_log}
sender #{inspect(sender)}
end
"""
)
|> create_new_otp_sender(sender, options)
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
defp create_new_otp_sender(igniter, sender, _options) do
Igniter.Project.Module.create_module(
igniter,
sender,
~s'''
@moduledoc """
Sends a one-time password code to the user.
"""
use AshAuthentication.Sender
@impl true
def send(user, otp_code, _opts) do
# The `user` argument is the user record matching the identity that
# was submitted. The `otp_code` is the short code that should be
# sent to them — typically by email or SMS.
IO.puts("""
Hello, \#{user.email}! Your sign-in code is:
\#{otp_code}
This code expires in 10 minutes.
""")
end
''',
on_exists: :warning
)
end
end
else
defmodule Mix.Tasks.AshAuthentication.AddStrategy.Otp do
@shortdoc "Adds one-time password (OTP) authentication to your user resource"
@moduledoc @shortdoc
use Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'ash_authentication.add_strategy.otp' 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