mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-19 21:03:23 +12:00
improvement(Confirmation): Confirmation is not a strategy. (#46)
* improvement(Confirmation): Confirmation is not a strategy. * improvement(Confirmation): Support more than one confirmation entity. * chore: move FIXME doc to issue.
This commit is contained in:
parent
eec87a0bea
commit
e88a516b22
18 changed files with 330 additions and 228 deletions
|
@ -85,7 +85,7 @@ defmodule AshAuthentication do
|
|||
AshAuthentication.Transformer,
|
||||
AshAuthentication.Strategy.Password.Transformer,
|
||||
AshAuthentication.Strategy.OAuth2.Transformer,
|
||||
AshAuthentication.Strategy.Confirmation.Transformer
|
||||
AshAuthentication.AddOn.Confirmation.Transformer
|
||||
]
|
||||
|
||||
require Ash.Query
|
||||
|
|
139
lib/ash_authentication/add_ons/confirmation.ex
Normal file
139
lib/ash_authentication/add_ons/confirmation.ex
Normal file
|
@ -0,0 +1,139 @@
|
|||
defmodule AshAuthentication.AddOn.Confirmation do
|
||||
import AshAuthentication.Dsl
|
||||
|
||||
@moduledoc """
|
||||
Confirmation support.
|
||||
|
||||
Sometimes when creating a new user, or changing a sensitive attribute (such as
|
||||
their email address) you may want to wait for the user to confirm by way of
|
||||
sending them a confirmation token to prove that it was really them that took
|
||||
the action.
|
||||
|
||||
In order to add confirmation to your resource, it must been the following
|
||||
minimum requirements:
|
||||
|
||||
1. Have a primary key
|
||||
2. Have at least one attribute you wish to confirm
|
||||
3. Tokens must be enabled
|
||||
|
||||
## Example
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Accounts.User do
|
||||
use Ash.Resource,
|
||||
extensions: [AshAuthentication]
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
attribute :email, :ci_string, allow_nil?: false
|
||||
end
|
||||
|
||||
authentication do
|
||||
api MyApp.Accounts
|
||||
|
||||
add_ons do
|
||||
confirmation :confirm do
|
||||
monitor_fields [:email]
|
||||
end
|
||||
end
|
||||
|
||||
strategies do
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
A `confirmed_at` attribute will be added to your resource if it's not already
|
||||
present (see `confirmed_at_field` in the DSL documentation).
|
||||
|
||||
## Actions
|
||||
|
||||
By default confirmation will add an action which updates the `confirmed_at`
|
||||
attribute as well as retrieving previously stored changes and applying them to
|
||||
the resource.
|
||||
|
||||
If you wish to perform the confirm action directly from your code you can do
|
||||
so via the `AshAuthentication.Strategy` protocol.
|
||||
|
||||
### Example
|
||||
|
||||
iex> strategy = Info.strategy!(Example.User, :confirm)
|
||||
...> {:ok, user} = Strategy.action(strategy, :confirm, %{"confirm" => confirmation_token()})
|
||||
...> user.confirmed_at >= one_second_ago()
|
||||
true
|
||||
|
||||
## Plugs
|
||||
|
||||
Confirmation provides a single endpoint for the `:confirm` phase. If you wish
|
||||
to interact with the plugs directly, you can do so via the
|
||||
`AshAuthentication.Strategy` protocol.
|
||||
|
||||
### Example
|
||||
|
||||
iex> strategy = Info.strategy!(Example.User, :confirm)
|
||||
...> conn = conn(:get, "/user/confirm", %{"confirm" => confirmation_token()})
|
||||
...> conn = Strategy.plug(strategy, :confirm, conn)
|
||||
...> {_conn, {:ok, user}} = Plug.Helpers.get_authentication_result(conn)
|
||||
...> user.confirmed_at >= one_second_ago()
|
||||
true
|
||||
|
||||
## DSL Documentation
|
||||
|
||||
#{Spark.Dsl.Extension.doc_entity(strategy(:confirmation))}
|
||||
"""
|
||||
|
||||
defstruct token_lifetime: nil,
|
||||
monitor_fields: [],
|
||||
confirmed_at_field: :confirmed_at,
|
||||
confirm_on_create?: true,
|
||||
confirm_on_update?: true,
|
||||
inhibit_updates?: false,
|
||||
sender: nil,
|
||||
confirm_action_name: :confirm,
|
||||
resource: nil,
|
||||
provider: :confirmation,
|
||||
name: :confirm
|
||||
|
||||
alias Ash.Changeset
|
||||
alias AshAuthentication.{AddOn.Confirmation, Jwt}
|
||||
|
||||
@type t :: %Confirmation{
|
||||
token_lifetime: hours :: pos_integer,
|
||||
monitor_fields: [atom],
|
||||
confirmed_at_field: atom,
|
||||
confirm_on_create?: boolean,
|
||||
confirm_on_update?: boolean,
|
||||
inhibit_updates?: boolean,
|
||||
sender: nil | {module, keyword},
|
||||
confirm_action_name: atom,
|
||||
resource: module,
|
||||
provider: :confirmation,
|
||||
name: :confirm
|
||||
}
|
||||
|
||||
@doc """
|
||||
Generate a confirmation token for a changeset.
|
||||
|
||||
This will generate a token with the `"act"` claim set to the confirmation
|
||||
action for the strategy, and the `"chg"` claim will contain any changes.
|
||||
"""
|
||||
@spec confirmation_token(Confirmation.t(), Changeset.t()) :: {:ok, String.t()} | :error
|
||||
def confirmation_token(strategy, changeset) do
|
||||
changes =
|
||||
strategy.monitor_fields
|
||||
|> Stream.filter(&Changeset.changing_attribute?(changeset, &1))
|
||||
|> Stream.map(&{to_string(&1), to_string(Changeset.get_attribute(changeset, &1))})
|
||||
|> Map.new()
|
||||
|
||||
claims = %{"act" => strategy.confirm_action_name, "chg" => changes}
|
||||
token_lifetime = strategy.token_lifetime * 3600
|
||||
|
||||
case Jwt.token_for_user(changeset.data, claims, token_lifetime: token_lifetime) do
|
||||
{:ok, token, _claims} -> {:ok, token}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,12 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.Actions do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
||||
@moduledoc """
|
||||
Actions for the confirmation strategy.
|
||||
Actions for the confirmation add-on.
|
||||
|
||||
Provides the code interface for working with resources via confirmation.
|
||||
"""
|
||||
|
||||
alias Ash.{Changeset, Resource}
|
||||
alias AshAuthentication.{Errors.InvalidToken, Info, Jwt, Strategy.Confirmation}
|
||||
alias AshAuthentication.{AddOn.Confirmation, Errors.InvalidToken, Info, Jwt}
|
||||
|
||||
@doc """
|
||||
Attempt to confirm a user.
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.ConfirmChange do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
|
||||
@moduledoc """
|
||||
Performs a change based on the contents of a confirmation token.
|
||||
"""
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.ConfirmationHookChange do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
|
||||
@moduledoc """
|
||||
Triggers a confirmation flow when one of the monitored fields is changed.
|
||||
|
||||
|
@ -7,7 +7,7 @@ defmodule AshAuthentication.Strategy.Confirmation.ConfirmationHookChange do
|
|||
|
||||
use Ash.Resource.Change
|
||||
alias Ash.{Changeset, Resource.Change}
|
||||
alias AshAuthentication.{Info, Strategy.Confirmation}
|
||||
alias AshAuthentication.{AddOn.Confirmation, Info}
|
||||
|
||||
@doc false
|
||||
@impl true
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.Plug do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.Plug do
|
||||
@moduledoc """
|
||||
Handlers for incoming OAuth2 HTTP requests.
|
||||
"""
|
||||
|
||||
alias AshAuthentication.{Strategy, Strategy.Confirmation}
|
||||
alias AshAuthentication.{AddOn.Confirmation, Strategy}
|
||||
alias Plug.Conn
|
||||
import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2]
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
defimpl AshAuthentication.Strategy, for: AshAuthentication.Strategy.Confirmation do
|
||||
defimpl AshAuthentication.Strategy, for: AshAuthentication.AddOn.Confirmation do
|
||||
@moduledoc """
|
||||
Implementation of `AshAuthentication.Strategy` for
|
||||
`AshAuthentication.Strategy.Confirmation`.
|
||||
`AshAuthentication.AddOn.Confirmation`.
|
||||
"""
|
||||
|
||||
alias Ash.Resource
|
||||
alias AshAuthentication.{Info, Strategy, Strategy.Confirmation}
|
||||
alias AshAuthentication.{Info, Strategy, AddOn.Confirmation}
|
||||
alias Plug.Conn
|
||||
|
||||
@typedoc "The request phases supposed by this strategy"
|
|
@ -1,6 +1,6 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.Transformer do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.Transformer do
|
||||
@moduledoc """
|
||||
DSL transformer for confirmation strategy.
|
||||
DSL transformer for confirmation add-on.
|
||||
|
||||
Ensures that there is only ever one present and that it is correctly
|
||||
configured.
|
||||
|
@ -8,7 +8,7 @@ defmodule AshAuthentication.Strategy.Confirmation.Transformer do
|
|||
|
||||
use Spark.Dsl.Transformer
|
||||
alias Ash.{Resource, Type}
|
||||
alias AshAuthentication.{GenerateTokenChange, Info, Sender, Strategy.Confirmation}
|
||||
alias AshAuthentication.{AddOn.Confirmation, GenerateTokenChange, Info, Sender}
|
||||
alias Spark.{Dsl.Transformer, Error.DslError}
|
||||
import AshAuthentication.Utils
|
||||
import AshAuthentication.Validations
|
||||
|
@ -37,22 +37,14 @@ defmodule AshAuthentication.Strategy.Confirmation.Transformer do
|
|||
| :halt
|
||||
def transform(dsl_state) do
|
||||
dsl_state
|
||||
|> Info.authentication_strategies()
|
||||
|> Info.authentication_add_ons()
|
||||
|> Enum.filter(&is_struct(&1, Confirmation))
|
||||
|> case do
|
||||
[] ->
|
||||
{:ok, dsl_state}
|
||||
|
||||
[strategy] ->
|
||||
transform_strategy(strategy, dsl_state)
|
||||
|
||||
[_ | _] ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
path: [:authentication, :strategies, :confirmation],
|
||||
message: "Multiple confirmation strategies are not supported"
|
||||
)}
|
||||
|> Enum.reduce_while({:ok, dsl_state}, fn strategy, {:ok, dsl_state} ->
|
||||
case transform_strategy(strategy, dsl_state) do
|
||||
{:ok, dsl_state} -> {:cont, {:ok, dsl_state}}
|
||||
{:error, reason} -> {:halt, {:error, reason}}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp transform_strategy(strategy, dsl_state) do
|
||||
|
@ -79,7 +71,7 @@ defmodule AshAuthentication.Strategy.Confirmation.Transformer do
|
|||
dsl_state =
|
||||
dsl_state
|
||||
|> Transformer.replace_entity(
|
||||
[:authentication, :strategies],
|
||||
[:authentication, :add_ons],
|
||||
%{strategy | resource: resource},
|
||||
&(&1.name == strategy.name)
|
||||
)
|
||||
|
@ -88,7 +80,7 @@ defmodule AshAuthentication.Strategy.Confirmation.Transformer do
|
|||
else
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
{:error,
|
||||
DslError.exception(path: [:authentication, :strategies, :confirmation], message: reason)}
|
||||
DslError.exception(path: [:authentication, :add_ons, :confirmation], message: reason)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
@ -96,7 +88,7 @@ defmodule AshAuthentication.Strategy.Confirmation.Transformer do
|
|||
:error ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
path: [:authentication, :strategies, :confirmation],
|
||||
path: [:authentication, :add_ons, :confirmation],
|
||||
message: "Configuration error"
|
||||
)}
|
||||
end
|
||||
|
@ -106,7 +98,7 @@ defmodule AshAuthentication.Strategy.Confirmation.Transformer do
|
|||
do:
|
||||
{:error,
|
||||
DslError.exception(
|
||||
path: [:authentication, :strategies, :confirmation],
|
||||
path: [:authentication, :add_ons, :confirmation],
|
||||
message: "You should be monitoring at least one field"
|
||||
)}
|
||||
|
|
@ -11,7 +11,7 @@ defmodule AshAuthentication.Dsl do
|
|||
alias Ash.{Api, Resource}
|
||||
|
||||
alias AshAuthentication.{
|
||||
Strategy.Confirmation,
|
||||
AddOn.Confirmation,
|
||||
Strategy.OAuth2,
|
||||
Strategy.Password
|
||||
}
|
||||
|
@ -32,7 +32,18 @@ defmodule AshAuthentication.Dsl do
|
|||
]
|
||||
]
|
||||
|
||||
@default_lifetime_days 14
|
||||
@shared_addon_options [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
Uniquely identifies the add-on.
|
||||
""",
|
||||
required: true
|
||||
]
|
||||
]
|
||||
|
||||
@default_token_lifetime_days 14
|
||||
@default_confirmation_lifetime_days 3
|
||||
|
||||
@secret_type {:or,
|
||||
[
|
||||
|
@ -122,9 +133,9 @@ defmodule AshAuthentication.Dsl do
|
|||
probably set this to a reasonably long time to ensure
|
||||
a good user experience.
|
||||
|
||||
Defaults to #{@default_lifetime_days} days.
|
||||
Defaults to #{@default_token_lifetime_days} days.
|
||||
""",
|
||||
default: @default_lifetime_days * 24
|
||||
default: @default_token_lifetime_days * 24
|
||||
],
|
||||
revocation_resource: [
|
||||
type: {:behaviour, Resource},
|
||||
|
@ -144,7 +155,13 @@ defmodule AshAuthentication.Dsl do
|
|||
describe: "Configure authentication strategies on this resource",
|
||||
entities: [
|
||||
strategy(:password),
|
||||
strategy(:oauth2),
|
||||
strategy(:oauth2)
|
||||
]
|
||||
},
|
||||
%Section{
|
||||
name: :add_ons,
|
||||
describe: "Additional add-ons related to, but not providing authentication",
|
||||
entities: [
|
||||
strategy(:confirmation)
|
||||
]
|
||||
}
|
||||
|
@ -525,8 +542,110 @@ defmodule AshAuthentication.Dsl do
|
|||
%Entity{
|
||||
name: :confirmation,
|
||||
describe: "User confirmation flow",
|
||||
args: [:name],
|
||||
target: Confirmation,
|
||||
schema: Confirmation.schema()
|
||||
schema:
|
||||
OptionsHelpers.merge_schemas(
|
||||
[
|
||||
token_lifetime: [
|
||||
type: :pos_integer,
|
||||
doc: """
|
||||
How long should the confirmation token be valid, in hours.
|
||||
|
||||
Defaults to #{@default_confirmation_lifetime_days} days.
|
||||
""",
|
||||
default: @default_confirmation_lifetime_days * 24
|
||||
],
|
||||
monitor_fields: [
|
||||
type: {:list, :atom},
|
||||
doc: """
|
||||
A list of fields to monitor for changes (eg `[:email, :phone_number]`).
|
||||
|
||||
The confirmation will only be sent when one of these fields are changed.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
confirmed_at_field: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The name of a field to store the time that the last confirmation took
|
||||
place.
|
||||
|
||||
This attribute will be dynamically added to the resource if not already
|
||||
present.
|
||||
""",
|
||||
default: :confirmed_at
|
||||
],
|
||||
confirm_on_create?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Generate and send a confirmation token when a new resource is created?
|
||||
|
||||
Will only trigger when a create action is executed _and_ one of the
|
||||
monitored fields is being set.
|
||||
""",
|
||||
default: true
|
||||
],
|
||||
confirm_on_update?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Generate and send a confirmation token when a resource is changed?
|
||||
|
||||
Will only trigger when an update action is executed _and_ one of the
|
||||
monitored fields is being set.
|
||||
""",
|
||||
default: true
|
||||
],
|
||||
inhibit_updates?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Wait until confirmation is received before actually changing a monitored
|
||||
field?
|
||||
|
||||
If a change to a monitored field is detected, then the change is stored
|
||||
in the token resource and the changeset updated to not make the
|
||||
requested change. When the token is confirmed, the change will be
|
||||
applied.
|
||||
|
||||
This could be potentially weird for your users, but useful in the case
|
||||
of a user changing their email address or phone number where you want
|
||||
to verify that the new contact details are reachable.
|
||||
""",
|
||||
default: true
|
||||
],
|
||||
sender: [
|
||||
type:
|
||||
{:spark_function_behaviour, AshAuthentication.Sender,
|
||||
{AshAuthentication.SenderFunction, 2}},
|
||||
doc: """
|
||||
How to send the confirmation instructions to the user.
|
||||
|
||||
Allows you to glue sending of confirmation instructions to
|
||||
[swoosh](https://hex.pm/packages/swoosh),
|
||||
[ex_twilio](https://hex.pm/packages/ex_twilio) or whatever notification
|
||||
system is appropriate for your application.
|
||||
|
||||
Accepts a module, module and opts, or a function that takes a record,
|
||||
reset token and options.
|
||||
|
||||
See `AshAuthentication.Sender` for more information.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
confirm_action_name: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The name of the action to use when performing confirmation.
|
||||
|
||||
If this action is not already present on the resource, it will be
|
||||
created for you.
|
||||
""",
|
||||
default: :confirm
|
||||
]
|
||||
],
|
||||
@shared_addon_options,
|
||||
"Shared options"
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,7 @@ defmodule AshAuthentication.Info do
|
|||
def strategy(dsl_or_resource, name) do
|
||||
dsl_or_resource
|
||||
|> authentication_strategies()
|
||||
|> Stream.concat(authentication_add_ons(dsl_or_resource))
|
||||
|> Enum.find_value(:error, fn strategy ->
|
||||
if strategy.name == name, do: {:ok, strategy}
|
||||
end)
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation do
|
||||
@default_lifetime_days 3
|
||||
|
||||
@moduledoc """
|
||||
Strategy for authenticating sensitive changes.
|
||||
|
||||
Sometimes when creating a new user, or changing a sensitive attribute (such as
|
||||
their email address) you may want to for the user to confirm by way of sending
|
||||
them a confirmation token to prove that it was really them that took the
|
||||
action.
|
||||
|
||||
See the DSL documentation for `AshAuthentication` for information on how to
|
||||
configure it.
|
||||
"""
|
||||
|
||||
defstruct token_lifetime: nil,
|
||||
monitor_fields: [],
|
||||
confirmed_at_field: :confirmed_at,
|
||||
confirm_on_create?: true,
|
||||
confirm_on_update?: true,
|
||||
inhibit_updates?: false,
|
||||
sender: nil,
|
||||
confirm_action_name: :confirm,
|
||||
resource: nil,
|
||||
provider: :confirmation,
|
||||
name: :confirm
|
||||
|
||||
alias Ash.Changeset
|
||||
alias AshAuthentication.{Jwt, Strategy.Confirmation}
|
||||
|
||||
@type t :: %Confirmation{
|
||||
token_lifetime: hours :: pos_integer,
|
||||
monitor_fields: [atom],
|
||||
confirmed_at_field: atom,
|
||||
confirm_on_create?: boolean,
|
||||
confirm_on_update?: boolean,
|
||||
inhibit_updates?: boolean,
|
||||
sender: nil | {module, keyword},
|
||||
confirm_action_name: atom,
|
||||
resource: module,
|
||||
provider: :confirmation,
|
||||
name: :confirm
|
||||
}
|
||||
|
||||
@doc """
|
||||
Generate a confirmation token for a changeset.
|
||||
|
||||
This will generate a token with the `"act"` claim set to the confirmation
|
||||
action for the strategy, and the `"chg"` claim will contain any changes.
|
||||
|
||||
FIXME: The "chg" claim should encrypt the contents of the changes so as to not
|
||||
leak users' private details.
|
||||
"""
|
||||
@spec confirmation_token(Confirmation.t(), Changeset.t()) :: {:ok, String.t()} | :error
|
||||
def confirmation_token(strategy, changeset) do
|
||||
changes =
|
||||
strategy.monitor_fields
|
||||
|> Stream.filter(&Changeset.changing_attribute?(changeset, &1))
|
||||
|> Stream.map(&{to_string(&1), to_string(Changeset.get_attribute(changeset, &1))})
|
||||
|> Map.new()
|
||||
|
||||
claims = %{"act" => strategy.confirm_action_name, "chg" => changes}
|
||||
token_lifetime = strategy.token_lifetime * 3600
|
||||
|
||||
case Jwt.token_for_user(changeset.data, claims, token_lifetime: token_lifetime) do
|
||||
{:ok, token, _claims} -> {:ok, token}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec schema :: keyword
|
||||
def schema do
|
||||
[
|
||||
token_lifetime: [
|
||||
type: :pos_integer,
|
||||
doc: """
|
||||
How long should the confirmation token be valid, in hours.
|
||||
|
||||
Defaults to #{@default_lifetime_days} days.
|
||||
""",
|
||||
default: @default_lifetime_days * 24
|
||||
],
|
||||
monitor_fields: [
|
||||
type: {:list, :atom},
|
||||
doc: """
|
||||
A list of fields to monitor for changes (eg `[:email, :phone_number]`).
|
||||
|
||||
The confirmation will only be sent when one of these fields are changed.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
confirmed_at_field: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The name of a field to store the time that the last confirmation took
|
||||
place.
|
||||
|
||||
This attribute will be dynamically added to the resource if not already
|
||||
present.
|
||||
""",
|
||||
default: :confirmed_at
|
||||
],
|
||||
confirm_on_create?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Generate and send a confirmation token when a new resource is created?
|
||||
|
||||
Will only trigger when a create action is executed _and_ one of the
|
||||
monitored fields is being set.
|
||||
""",
|
||||
default: true
|
||||
],
|
||||
confirm_on_update?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Generate and send a confirmation token when a resource is changed?
|
||||
|
||||
Will only trigger when an update action is executed _and_ one of the
|
||||
monitored fields is being set.
|
||||
""",
|
||||
default: true
|
||||
],
|
||||
inhibit_updates?: [
|
||||
type: :boolean,
|
||||
doc: """
|
||||
Wait until confirmation is received before actually changing a monitored
|
||||
field?
|
||||
|
||||
If a change to a monitored field is detected, then the change is stored
|
||||
in the confirmation token and the changeset updated to not make the
|
||||
requested change. When the token is confirmed, the change will be
|
||||
applied.
|
||||
|
||||
This could be potentially weird for your users, but useful in the case
|
||||
of a user changing their email address or phone number where you want
|
||||
to verify that the new contact details are reachable.
|
||||
""",
|
||||
default: false
|
||||
],
|
||||
sender: [
|
||||
type:
|
||||
{:spark_function_behaviour, AshAuthentication.Sender,
|
||||
{AshAuthentication.SenderFunction, 2}},
|
||||
doc: """
|
||||
How to send the confirmation instructions to the user.
|
||||
|
||||
Allows you to glue sending of confirmation instructions to
|
||||
[swoosh](https://hex.pm/packages/swoosh),
|
||||
[ex_twilio](https://hex.pm/packages/ex_twilio) or whatever notification
|
||||
system is appropriate for your application.
|
||||
|
||||
Accepts a module, module and opts, or a function that takes a record,
|
||||
reset token and options.
|
||||
|
||||
See `AshAuthentication.Sender` for more information.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
confirm_action_name: [
|
||||
type: :atom,
|
||||
doc: """
|
||||
The name of the action to use when performing confirmation.
|
||||
|
||||
If this action is not already present on the resource, it will be
|
||||
created for you.
|
||||
""",
|
||||
default: :confirm
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
6
mix.exs
6
mix.exs
|
@ -38,11 +38,13 @@ defmodule AshAuthentication.MixProject do
|
|||
AshAuthentication.Strategy.Password,
|
||||
AshAuthentication.Strategy.OAuth2
|
||||
],
|
||||
"Add ons": [
|
||||
AshAuthentication.AddOn.Confirmation
|
||||
],
|
||||
Cryptography: [
|
||||
AshAuthentication.HashProvider,
|
||||
AshAuthentication.BcryptProvider,
|
||||
AshAuthentication.Jwt,
|
||||
AshAuthentication.Jwt.Config
|
||||
AshAuthentication.Jwt
|
||||
],
|
||||
Plug: ~r/^AshAuthentication\.Plug.*/,
|
||||
Internals: ~r/^AshAuthentication.*/
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.ActionsTest do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
|
||||
@moduledoc false
|
||||
use DataCase, async: true
|
||||
|
||||
alias Ash.Changeset
|
||||
alias AshAuthentication.{Info, Strategy.Confirmation, Strategy.Confirmation.Actions}
|
||||
alias AshAuthentication.{AddOn.Confirmation, AddOn.Confirmation.Actions, Info}
|
||||
|
||||
describe "confirm/2" do
|
||||
test "it returns an error when there is no corresponding user" do
|
|
@ -1,8 +1,10 @@
|
|||
defmodule AshAuthentication.Strategy.ConfirmationTest do
|
||||
defmodule AshAuthentication.AddOn.ConfirmationTest do
|
||||
@moduledoc false
|
||||
use DataCase, async: true
|
||||
import Plug.Test
|
||||
alias Ash.Changeset
|
||||
alias AshAuthentication.{Info, Jwt, Strategy.Confirmation}
|
||||
alias AshAuthentication.{AddOn.Confirmation, Info, Jwt, Plug, Strategy}
|
||||
|
||||
doctest Confirmation
|
||||
|
||||
describe "confirmation_token/2" do
|
||||
|
@ -19,4 +21,21 @@ defmodule AshAuthentication.Strategy.ConfirmationTest do
|
|||
assert claims["chg"] == %{"username" => new_username}
|
||||
end
|
||||
end
|
||||
|
||||
def confirmation_token do
|
||||
{:ok, strategy} = Info.strategy(Example.User, :confirm)
|
||||
user = build_user()
|
||||
|
||||
new_username = username()
|
||||
changeset = Changeset.for_update(user, :update, %{"username" => new_username})
|
||||
|
||||
assert {:ok, token} = Confirmation.confirmation_token(strategy, changeset)
|
||||
token
|
||||
end
|
||||
|
||||
def one_second_ago do
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-1, :second)
|
||||
|> DateTime.to_unix()
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.PlugTest do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.PlugTest do
|
||||
@moduledoc false
|
||||
use DataCase, async: true
|
||||
import Plug.Test
|
||||
|
@ -6,10 +6,10 @@ defmodule AshAuthentication.Strategy.Confirmation.PlugTest do
|
|||
alias Ash.Changeset
|
||||
|
||||
alias AshAuthentication.{
|
||||
AddOn.Confirmation,
|
||||
AddOn.Confirmation.Plug,
|
||||
Info,
|
||||
Plug.Helpers,
|
||||
Strategy.Confirmation,
|
||||
Strategy.Confirmation.Plug
|
||||
Plug.Helpers
|
||||
}
|
||||
|
||||
describe "confirm/2" do
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AshAuthentication.Strategy.Confirmation.StrategyTest do
|
||||
defmodule AshAuthentication.AddOn.Confirmation.StrategyTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias AshAuthentication.{Info, Strategy, Strategy.Confirmation}
|
||||
alias AshAuthentication.{AddOn.Confirmation, Info, Strategy}
|
||||
|
||||
use Mimic
|
||||
import Plug.Test
|
|
@ -105,8 +105,8 @@ defmodule Example.User do
|
|||
revocation_resource(Example.TokenRevocation)
|
||||
end
|
||||
|
||||
strategies do
|
||||
confirmation do
|
||||
add_ons do
|
||||
confirmation :confirm do
|
||||
monitor_fields([:username])
|
||||
inhibit_updates?(true)
|
||||
|
||||
|
@ -114,7 +114,9 @@ defmodule Example.User do
|
|||
Logger.debug("Confirmation request for user #{user.username}, token #{inspect(token)}")
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
strategies do
|
||||
password :password do
|
||||
resettable do
|
||||
sender(fn user, token ->
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Mimic.copy(AshAuthentication.AddOn.Confirmation.Actions)
|
||||
Mimic.copy(AshAuthentication.AddOn.Confirmation.Plug)
|
||||
Mimic.copy(AshAuthentication.Plug.Defaults)
|
||||
Mimic.copy(AshAuthentication.Plug.Helpers)
|
||||
Mimic.copy(AshAuthentication.Strategy.Confirmation.Actions)
|
||||
Mimic.copy(AshAuthentication.Strategy.Confirmation.Plug)
|
||||
Mimic.copy(AshAuthentication.Strategy.OAuth2.Actions)
|
||||
Mimic.copy(AshAuthentication.Strategy.OAuth2.Plug)
|
||||
Mimic.copy(AshAuthentication.Strategy.Password.Actions)
|
||||
|
|
Loading…
Reference in a new issue