mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-19 12:52:55 +12:00
improvement: Allow all token lifetimes to be specified with a time unit.
Now any DSL option which allows the configuring of a token lifetime can take _either_ a positive integer in it's previous default unit or a tuple containing a positive integer and a unit. Closes #376. Additionally includes switching the resettable entity to being a singleton since that feature didn't exist when I started.
This commit is contained in:
parent
a876a4453d
commit
7b607896eb
24 changed files with 149 additions and 69 deletions
|
@ -95,8 +95,9 @@ User confirmation flow
|
|||
|
||||
* `:name` (`t:atom/0`) - Required. Uniquely identifies the add-on.
|
||||
|
||||
* `:token_lifetime` (`t:pos_integer/0`) - How long should the confirmation token be valid, in hours.
|
||||
Defaults to 3 days. The default value is `72`.
|
||||
* `:token_lifetime` - How long should the confirmation token be valid.
|
||||
If no unit is provided, then hours is assumed.
|
||||
Defaults to 3 days. The default value is `{3, :days}`.
|
||||
|
||||
* `:monitor_fields` (list of `t:atom/0`) - Required. 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.
|
||||
|
@ -169,7 +170,7 @@ User confirmation flow
|
|||
| --- | --- | --- | --- |
|
||||
| `monitor_fields`* | `list(atom)` | | 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. |
|
||||
| `sender`* | `(any, any, any -> any) \| module` | | 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. The options will be a keyword list containing the original changeset, before any changes were inhibited. This allows you to send an email to the user's new email address if it is being changed for example. See `AshAuthentication.Sender` for more information. |
|
||||
| `token_lifetime` | `pos_integer` | `72` | How long should the confirmation token be valid, in hours. Defaults to 3 days. |
|
||||
| `token_lifetime` | `pos_integer \| {pos_integer, :days \| :hours \| :minutes \| :seconds}` | `{3, :days}` | How long should the confirmation token be valid. If no unit is provided, then hours is assumed. Defaults to 3 days. |
|
||||
| `confirmed_at_field` | `atom` | `:confirmed_at` | 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. |
|
||||
| `confirm_on_create?` | `boolean` | `true` | 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. |
|
||||
| `confirm_on_update?` | `boolean` | `true` | 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. |
|
||||
|
|
|
@ -104,7 +104,8 @@ Strategy for authenticating using local users with a magic link
|
|||
* `:identity_field` (`t:atom/0`) - The name of the attribute which uniquely identifies the user.
|
||||
Usually something like `username` or `email_address`. The default value is `:username`.
|
||||
|
||||
* `:token_lifetime` (`t:pos_integer/0`) - How long the sign in token is valid, in minutes. The default value is `10`.
|
||||
* `:token_lifetime` - How long the sign in token is valid.
|
||||
If no unit is provided, then `minutes` is assumed. The default value is `{10, :minutes}`.
|
||||
|
||||
* `:request_action_name` (`t:atom/0`) - The name to use for the request action.
|
||||
If not present it will be generated by prepending the strategy name
|
||||
|
@ -148,7 +149,7 @@ Strategy for authenticating using local users with a magic link
|
|||
| --- | --- | --- | --- |
|
||||
| `sender`* | `(any, any, any -> any) \| module` | | How to send the magic link to the user. Allows you to glue sending of magic links 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. |
|
||||
| `identity_field` | `atom` | `:username` | The name of the attribute which uniquely identifies the user. Usually something like `username` or `email_address`. |
|
||||
| `token_lifetime` | `pos_integer` | `10` | How long the sign in token is valid, in minutes. |
|
||||
| `token_lifetime` | `pos_integer \| {pos_integer, :days \| :hours \| :minutes \| :seconds}` | `{10, :minutes}` | How long the sign in token is valid. If no unit is provided, then `minutes` is assumed. |
|
||||
| `request_action_name` | `atom` | | The name to use for the request action. If not present it will be generated by prepending the strategy name with `request_`. |
|
||||
| `single_use_token?` | `boolean` | `true` | Automatically revoke the token once it's been used for sign in. |
|
||||
| `sign_in_action_name` | `atom` | | The name to use for the sign in action. If not present it will be generated by prepending the strategy name with `sign_in_with_`. |
|
||||
|
|
|
@ -150,7 +150,8 @@ end
|
|||
by `ash_authentication_phoenix` (since 1.7) to support signing in in a liveview, and then redirecting
|
||||
with a valid token to a controller action, allowing the liveview to show invalid username/password errors. The default value is `false`.
|
||||
|
||||
* `:sign_in_token_lifetime` (`t:pos_integer/0`) - A lifetime (in seconds) for which a generated sign in token will be valid, if `sign_in_tokens_enabled?`. The default value is `60`.
|
||||
* `:sign_in_token_lifetime` - A lifetime for which a generated sign in token will be valid, if `sign_in_tokens_enabled?`.
|
||||
If no unit is specified, defaults to `:seconds`. The default value is `{60, :seconds}`.
|
||||
|
||||
|
||||
|
||||
|
@ -162,8 +163,9 @@ Configure password reset options for the resource
|
|||
|
||||
|
||||
|
||||
* `:token_lifetime` (`t:pos_integer/0`) - How long should the reset token be valid, in hours.
|
||||
Defaults to 3 days. The default value is `72`.
|
||||
* `:token_lifetime` - How long should the reset token be valid.
|
||||
If no unit is provided `:hours` is assumed.
|
||||
Defaults to 3 days. The default value is `{3, :days}`.
|
||||
|
||||
* `:request_password_reset_action_name` (`t:atom/0`) - The name to use for the action which generates a password reset token.
|
||||
If not present it will be generated by prepending the strategy name
|
||||
|
@ -228,7 +230,7 @@ end
|
|||
| `sign_in_action_name` | `atom` | | The name to use for the sign in action. If not present it will be generated by prepending the strategy name with `sign_in_with_`. |
|
||||
| `sign_in_enabled?` | `boolean` | `true` | If you do not want new users to be able to sign in using this strategy, set this to false. |
|
||||
| `sign_in_tokens_enabled?` | `boolean` | `false` | Whether or not to support generating short lived sign in tokens. Requires the resource to have tokens enabled. There is no drawback to supporting this, and in the future this default will change from `false` to `true`. Sign in tokens can be generated on request by setting the `:token_type` context to `:sign_in` when calling the sign in action. You might do this when you need to generate a short lived token to be exchanged for a real token using the `validate_sign_in_token` route. This is used, for example, by `ash_authentication_phoenix` (since 1.7) to support signing in in a liveview, and then redirecting with a valid token to a controller action, allowing the liveview to show invalid username/password errors. |
|
||||
| `sign_in_token_lifetime` | `pos_integer` | `60` | A lifetime (in seconds) for which a generated sign in token will be valid, if `sign_in_tokens_enabled?`. |
|
||||
| `sign_in_token_lifetime` | `pos_integer \| {pos_integer, :days \| :hours \| :minutes \| :seconds}` | `{60, :seconds}` | A lifetime for which a generated sign in token will be valid, if `sign_in_tokens_enabled?`. If no unit is specified, defaults to `:seconds`. |
|
||||
|
||||
|
||||
## authentication.strategies.password.resettable
|
||||
|
@ -245,7 +247,7 @@ Configure password reset options for the resource
|
|||
| Name | Type | Default | Docs |
|
||||
| --- | --- | --- | --- |
|
||||
| `sender`* | `(any, any, any -> any) \| module` | | How to send the password reset instructions to the user. Allows you to glue sending of reset 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. |
|
||||
| `token_lifetime` | `pos_integer` | `72` | How long should the reset token be valid, in hours. Defaults to 3 days. |
|
||||
| `token_lifetime` | `pos_integer \| {pos_integer, :days \| :hours \| :minutes \| :seconds}` | `{3, :days}` | How long should the reset token be valid. If no unit is provided `:hours` is assumed. Defaults to 3 days. |
|
||||
| `request_password_reset_action_name` | `atom` | | The name to use for the action which generates a password reset token. If not present it will be generated by prepending the strategy name with `request_password_reset_with_`. |
|
||||
| `password_reset_action_name` | `atom` | | The name to use for the action which actually resets the user's password. If not present it will be generated by prepending the strategy name with `password_reset_with_`. |
|
||||
|
||||
|
|
|
@ -163,11 +163,13 @@ Configure JWT settings for this resource
|
|||
Available signing algorithms are;
|
||||
EdDSA, Ed448ph, Ed448, Ed25519ph, Ed25519, PS512, PS384, PS256, ES512, ES384, ES256, RS512, RS384, RS256, HS512, HS384 and HS256. The default value is `"HS256"`.
|
||||
|
||||
* `:token_lifetime` (`t:pos_integer/0`) - How long a token should be valid, in hours.
|
||||
* `:token_lifetime` - How long a token should be valid.
|
||||
Since refresh tokens are not yet supported, you should
|
||||
probably set this to a reasonably long time to ensure
|
||||
a good user experience.
|
||||
Defaults to 14 days. The default value is `336`.
|
||||
You can either provide a tuple with a time unit, or a positive
|
||||
integer, in which case the unit is assumed to be hours.
|
||||
Defaults to 14 days. The default value is `{14, :days}`.
|
||||
|
||||
* `:token_resource` - Required. The resource used to store token information.
|
||||
If token generation is enabled for this resource, we need a place to
|
||||
|
@ -266,7 +268,7 @@ Configure JWT settings for this resource
|
|||
| `store_all_tokens?` | `boolean` | `false` | Store all tokens in the `token_resource`? Some applications need to keep track of all tokens issued to any user. This is optional behaviour with `ash_authentication` in order to preserve as much performance as possible. |
|
||||
| `require_token_presence_for_authentication?` | `boolean` | `false` | Require a locally-stored token for authentication? This inverts the token validation behaviour from requiring that tokens are not revoked to requiring any token presented by a client to be present in the token resource to be considered valid. Requires `store_all_tokens?` to be `true`. |
|
||||
| `signing_algorithm` | `String.t` | `"HS256"` | The algorithm to use for token signing. Available signing algorithms are; EdDSA, Ed448ph, Ed448, Ed25519ph, Ed25519, PS512, PS384, PS256, ES512, ES384, ES256, RS512, RS384, RS256, HS512, HS384 and HS256. |
|
||||
| `token_lifetime` | `pos_integer` | `336` | How long a token should be valid, in hours. Since refresh tokens are not yet supported, you should probably set this to a reasonably long time to ensure a good user experience. Defaults to 14 days. |
|
||||
| `token_lifetime` | `pos_integer \| {pos_integer, :days \| :hours \| :minutes \| :seconds}` | `{14, :days}` | How long a token should be valid. Since refresh tokens are not yet supported, you should probably set this to a reasonably long time to ensure a good user experience. You can either provide a tuple with a time unit, or a positive integer, in which case the unit is assumed to be hours. Defaults to 14 days. |
|
||||
| `signing_secret` | `(any, any -> any) \| module \| String.t` | | The secret used to sign tokens. Takes either a module which implements the `AshAuthentication.Secret` behaviour, a 2 arity anonymous function or a string. See the module documentation for `AshAuthentication.Secret` for more information. |
|
||||
|
||||
|
||||
|
|
|
@ -138,10 +138,9 @@ defmodule AshAuthentication.AddOn.Confirmation do
|
|||
{:ok, String.t()} | :error | {:error, any}
|
||||
def confirmation_token(strategy, changeset, user) do
|
||||
claims = %{"act" => strategy.confirm_action_name}
|
||||
token_lifetime = strategy.token_lifetime * 3600
|
||||
|
||||
with {:ok, token, _claims} <-
|
||||
Jwt.token_for_user(user, claims, token_lifetime: token_lifetime),
|
||||
Jwt.token_for_user(user, claims, token_lifetime: strategy.token_lifetime),
|
||||
:ok <- Confirmation.Actions.store_changes(strategy, token, changeset) do
|
||||
{:ok, token}
|
||||
end
|
||||
|
|
|
@ -31,12 +31,19 @@ defmodule AshAuthentication.AddOn.Confirmation.Dsl do
|
|||
required: true
|
||||
],
|
||||
token_lifetime: [
|
||||
type: :pos_integer,
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
:pos_integer,
|
||||
{:tuple, [:pos_integer, {:in, [:days, :hours, :minutes, :seconds]}]}
|
||||
]},
|
||||
doc: """
|
||||
How long should the confirmation token be valid, in hours.
|
||||
How long should the confirmation token be valid.
|
||||
If no unit is provided, then hours is assumed.
|
||||
|
||||
Defaults to #{@default_confirmation_lifetime_days} days.
|
||||
""",
|
||||
default: @default_confirmation_lifetime_days * 24
|
||||
default: {@default_confirmation_lifetime_days, :days}
|
||||
],
|
||||
monitor_fields: [
|
||||
type: {:list, :atom},
|
||||
|
|
|
@ -141,17 +141,25 @@ defmodule AshAuthentication.Dsl do
|
|||
default: hd(algorithms())
|
||||
],
|
||||
token_lifetime: [
|
||||
type: :pos_integer,
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
:pos_integer,
|
||||
{:tuple, [:pos_integer, {:in, [:days, :hours, :minutes, :seconds]}]}
|
||||
]},
|
||||
doc: """
|
||||
How long a token should be valid, in hours.
|
||||
How long a token should be valid.
|
||||
|
||||
Since refresh tokens are not yet supported, you should
|
||||
probably set this to a reasonably long time to ensure
|
||||
a good user experience.
|
||||
|
||||
You can either provide a tuple with a time unit, or a positive
|
||||
integer, in which case the unit is assumed to be hours.
|
||||
|
||||
Defaults to #{@default_token_lifetime_days} days.
|
||||
""",
|
||||
default: @default_token_lifetime_days * 24
|
||||
default: {@default_token_lifetime_days, :days}
|
||||
],
|
||||
token_resource: [
|
||||
type: {:or, [{:behaviour, Resource}, {:in, [false]}]},
|
||||
|
|
|
@ -20,7 +20,7 @@ defmodule AshAuthentication.Jwt.Config do
|
|||
opts
|
||||
|> Keyword.fetch(:token_lifetime)
|
||||
|> case do
|
||||
{:ok, hours} -> hours * 60 * 60
|
||||
{:ok, lifetime} -> lifetime_to_seconds(lifetime)
|
||||
:error -> token_lifetime(resource)
|
||||
end
|
||||
|
||||
|
@ -157,8 +157,13 @@ defmodule AshAuthentication.Jwt.Config do
|
|||
resource
|
||||
|> Info.authentication_tokens_token_lifetime()
|
||||
|> case do
|
||||
{:ok, hours} -> hours * 60 * 60
|
||||
{:ok, lifetime} -> lifetime_to_seconds(lifetime)
|
||||
:error -> Jwt.default_lifetime_hrs() * 60 * 60
|
||||
end
|
||||
end
|
||||
|
||||
defp lifetime_to_seconds({seconds, :seconds}), do: seconds
|
||||
defp lifetime_to_seconds({minutes, :minutes}), do: minutes * 60
|
||||
defp lifetime_to_seconds({hours, :hours}), do: hours * 60 * 60
|
||||
defp lifetime_to_seconds({days, :days}), do: days * 60 * 60 * 24
|
||||
end
|
||||
|
|
|
@ -108,7 +108,7 @@ defmodule AshAuthentication.Strategy.MagicLink do
|
|||
sign_in_action_name: nil,
|
||||
single_use_token?: true,
|
||||
strategy_module: __MODULE__,
|
||||
token_lifetime: 10,
|
||||
token_lifetime: {10, :minutes},
|
||||
token_param_name: :token
|
||||
|
||||
use AshAuthentication.Strategy.Custom, entity: Dsl.dsl()
|
||||
|
@ -141,7 +141,7 @@ defmodule AshAuthentication.Strategy.MagicLink do
|
|||
def request_token_for(strategy, user)
|
||||
when is_struct(strategy, __MODULE__) and is_struct(user, strategy.resource) do
|
||||
case Jwt.token_for_user(user, %{"act" => strategy.sign_in_action_name},
|
||||
token_lifetime: strategy.token_lifetime * 60,
|
||||
token_lifetime: strategy.token_lifetime,
|
||||
purpose: :magic_link
|
||||
) do
|
||||
{:ok, token, _claims} -> {:ok, token}
|
||||
|
|
|
@ -29,11 +29,18 @@ defmodule AshAuthentication.Strategy.MagicLink.Dsl do
|
|||
default: :username
|
||||
],
|
||||
token_lifetime: [
|
||||
type: :pos_integer,
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
:pos_integer,
|
||||
{:tuple, [:pos_integer, {:in, [:days, :hours, :minutes, :seconds]}]}
|
||||
]},
|
||||
doc: """
|
||||
How long the sign in token is valid, in minutes.
|
||||
How long the sign in token is valid.
|
||||
|
||||
If no unit is provided, then `minutes` is assumed.
|
||||
""",
|
||||
default: 10
|
||||
default: {10, :minutes}
|
||||
],
|
||||
request_action_name: [
|
||||
type: :atom,
|
||||
|
|
|
@ -21,6 +21,7 @@ defmodule AshAuthentication.Strategy.MagicLink.Transformer do
|
|||
),
|
||||
strategy <- maybe_set_sign_in_action_name(strategy),
|
||||
strategy <- maybe_set_request_action_name(strategy),
|
||||
strategy <- maybe_transform_token_lifetime(strategy),
|
||||
{:ok, dsl_state} <-
|
||||
maybe_build_action(
|
||||
dsl_state,
|
||||
|
@ -48,6 +49,11 @@ defmodule AshAuthentication.Strategy.MagicLink.Transformer do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_transform_token_lifetime(strategy) when is_integer(strategy.token_lifetime),
|
||||
do: %{strategy | token_lifetime: {strategy.token_lifetime, :minutes}}
|
||||
|
||||
defp maybe_transform_token_lifetime(strategy), do: strategy
|
||||
|
||||
# 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}")}
|
||||
|
|
|
@ -106,7 +106,7 @@ defmodule AshAuthentication.Strategy.Password do
|
|||
register_action_accept: [],
|
||||
register_action_name: nil,
|
||||
registration_enabled?: true,
|
||||
resettable: [],
|
||||
resettable: nil,
|
||||
resource: nil,
|
||||
sign_in_action_name: nil,
|
||||
sign_in_enabled?: true,
|
||||
|
@ -140,7 +140,7 @@ defmodule AshAuthentication.Strategy.Password do
|
|||
register_action_accept: [atom],
|
||||
register_action_name: atom,
|
||||
registration_enabled?: boolean,
|
||||
resettable: [Resettable.t()],
|
||||
resettable: nil | Resettable.t(),
|
||||
resource: module,
|
||||
sign_in_action_name: atom,
|
||||
sign_in_enabled?: boolean,
|
||||
|
@ -161,11 +161,11 @@ defmodule AshAuthentication.Strategy.Password do
|
|||
"""
|
||||
@spec reset_token_for(t(), Resource.record()) :: {:ok, String.t()} | :error
|
||||
def reset_token_for(
|
||||
%Password{resettable: [%Resettable{} = resettable]} = _strategy,
|
||||
%Password{resettable: %Resettable{} = resettable} = _strategy,
|
||||
user
|
||||
) do
|
||||
case Jwt.token_for_user(user, %{"act" => resettable.password_reset_action_name},
|
||||
token_lifetime: resettable.token_lifetime * 3600
|
||||
token_lifetime: resettable.token_lifetime
|
||||
) do
|
||||
{:ok, token, _claims} -> {:ok, token}
|
||||
:error -> :error
|
||||
|
|
|
@ -178,7 +178,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
|||
"""
|
||||
@spec reset_request(Password.t(), map, keyword) :: :ok | {:error, any}
|
||||
def reset_request(
|
||||
%Password{resettable: [%Password.Resettable{} = resettable]} = strategy,
|
||||
%Password{resettable: %Password.Resettable{} = resettable} = strategy,
|
||||
params,
|
||||
options
|
||||
) do
|
||||
|
@ -209,7 +209,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
|||
"""
|
||||
@spec reset(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
||||
def reset(
|
||||
%Password{resettable: [%Password.Resettable{} = resettable]} = strategy,
|
||||
%Password{resettable: %Password.Resettable{} = resettable} = strategy,
|
||||
params,
|
||||
options
|
||||
) do
|
||||
|
|
|
@ -28,6 +28,7 @@ defmodule AshAuthentication.Strategy.Password.Dsl do
|
|||
hide: [:name],
|
||||
target: Password,
|
||||
modules: [:hash_provider],
|
||||
singleton_entity_keys: [:resettable],
|
||||
schema: [
|
||||
name: [
|
||||
type: :atom,
|
||||
|
@ -148,10 +149,17 @@ defmodule AshAuthentication.Strategy.Password.Dsl do
|
|||
default: false
|
||||
],
|
||||
sign_in_token_lifetime: [
|
||||
type: :pos_integer,
|
||||
default: 60,
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
:pos_integer,
|
||||
{:tuple, [:pos_integer, {:in, [:days, :hours, :minutes, :seconds]}]}
|
||||
]},
|
||||
default: {60, :seconds},
|
||||
doc: """
|
||||
A lifetime (in seconds) for which a generated sign in token will be valid, if `sign_in_tokens_enabled?`.
|
||||
A lifetime for which a generated sign in token will be valid, if `sign_in_tokens_enabled?`.
|
||||
|
||||
If no unit is specified, defaults to `:seconds`.
|
||||
"""
|
||||
]
|
||||
],
|
||||
|
@ -163,13 +171,20 @@ defmodule AshAuthentication.Strategy.Password.Dsl do
|
|||
target: Password.Resettable,
|
||||
schema: [
|
||||
token_lifetime: [
|
||||
type: :pos_integer,
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
:pos_integer,
|
||||
{:tuple, [:pos_integer, {:in, [:days, :hours, :minutes, :seconds]}]}
|
||||
]},
|
||||
doc: """
|
||||
How long should the reset token be valid, in hours.
|
||||
How long should the reset token be valid.
|
||||
|
||||
If no unit is provided `:hours` is assumed.
|
||||
|
||||
Defaults to #{@default_token_lifetime_days} days.
|
||||
""",
|
||||
default: @default_token_lifetime_days * 24
|
||||
default: {@default_token_lifetime_days, :days}
|
||||
],
|
||||
request_password_reset_action_name: [
|
||||
type: :atom,
|
||||
|
|
|
@ -22,7 +22,7 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
|
|||
def prepare(query, _opts, _context) do
|
||||
strategy = Info.strategy_for_action!(query.resource, query.action.name)
|
||||
|
||||
if Enum.any?(strategy.resettable) do
|
||||
if strategy.resettable do
|
||||
identity_field = strategy.identity_field
|
||||
identity = Query.get_argument(query, identity_field)
|
||||
select_for_senders = Info.authentication_select_for_senders!(query.resource)
|
||||
|
@ -38,7 +38,7 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
|
|||
end
|
||||
end
|
||||
|
||||
defp after_action(_query, [user], %{resettable: [%{sender: {sender, send_opts}}]} = strategy) do
|
||||
defp after_action(_query, [user], %{resettable: %{sender: {sender, send_opts}}} = strategy) do
|
||||
case Password.reset_token_for(strategy, user) do
|
||||
{:ok, token} -> sender.send(user, token, send_opts)
|
||||
_ -> nil
|
||||
|
|
|
@ -14,7 +14,7 @@ defmodule AshAuthentication.Strategy.Password.ResetTokenValidation do
|
|||
with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
||||
token when is_binary(token) <- Changeset.get_argument(changeset, :reset_token),
|
||||
{:ok, %{"act" => token_action}, _} <- Jwt.verify(token, changeset.resource),
|
||||
{:ok, [resettable]} <- Map.fetch(strategy, :resettable),
|
||||
{:ok, resettable} <- Map.fetch(strategy, :resettable),
|
||||
true <- to_string(resettable.password_reset_action_name) == token_action do
|
||||
:ok
|
||||
else
|
||||
|
|
|
@ -34,7 +34,7 @@ defimpl AshAuthentication.Strategy, for: AshAuthentication.Strategy.Password do
|
|||
)
|
||||
|> maybe_append(strategy.registration_enabled?, :register)
|
||||
|> maybe_append(strategy.sign_in_enabled?, :sign_in)
|
||||
|> maybe_concat(Enum.any?(strategy.resettable), [:reset_request, :reset])
|
||||
|> maybe_concat(strategy.resettable, [:reset_request, :reset])
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
|||
|
||||
alias Ash.{Resource, Type}
|
||||
alias AshAuthentication.{GenerateTokenChange, Strategy, Strategy.Password}
|
||||
alias Spark.{Dsl.Transformer, Error.DslError}
|
||||
alias Spark.Dsl.Transformer
|
||||
import AshAuthentication.Strategy.Custom.Helpers
|
||||
import AshAuthentication.Utils
|
||||
import AshAuthentication.Validations
|
||||
|
@ -21,6 +21,7 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
|||
def transform(strategy, dsl_state) do
|
||||
with :ok <- validate_identity_field(strategy.identity_field, dsl_state),
|
||||
:ok <- validate_hashed_password_field(strategy.hashed_password_field, dsl_state),
|
||||
strategy <- maybe_transform_token_lifetime(strategy, :sign_in_token_lifetime, :seconds),
|
||||
strategy <-
|
||||
maybe_set_field_lazy(strategy, :register_action_name, &:"register_with_#{&1.name}"),
|
||||
{:ok, dsl_state} <-
|
||||
|
@ -73,11 +74,9 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
|||
end)
|
||||
|> then(fn dsl_state ->
|
||||
strategy
|
||||
|> Map.get(:resettable, [])
|
||||
|> Enum.flat_map(fn resettable ->
|
||||
~w[request_password_reset_action_name password_reset_action_name]a
|
||||
|> Enum.map(&Map.get(resettable, &1))
|
||||
end)
|
||||
|> Map.get(:resettable, %{})
|
||||
|> Map.take(~w[request_password_reset_action_name password_reset_action_name]a)
|
||||
|> Map.values()
|
||||
|> register_strategy_actions(dsl_state, strategy)
|
||||
end)
|
||||
|
||||
|
@ -85,6 +84,13 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_transform_token_lifetime(strategy, field, default_unit) do
|
||||
case Map.get(strategy, field) do
|
||||
ttl when is_integer(ttl) -> Map.put(strategy, field, {ttl, default_unit})
|
||||
_ -> strategy
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_identity_field(identity_field, dsl_state) do
|
||||
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
||||
{:ok, attribute} <- find_attribute(dsl_state, identity_field),
|
||||
|
@ -348,11 +354,11 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
|||
|
||||
defp maybe_maybe_build_action(false, dsl_state, _action_name, _builder), do: {:ok, dsl_state}
|
||||
|
||||
defp maybe_transform_resettable(dsl_state, %{resettable: []} = strategy),
|
||||
defp maybe_transform_resettable(dsl_state, %{resettable: nil} = strategy),
|
||||
do: {:ok, dsl_state, strategy}
|
||||
|
||||
# sobelow_skip ["DOS.BinToAtom"]
|
||||
defp maybe_transform_resettable(dsl_state, %{resettable: [resettable]} = strategy) do
|
||||
defp maybe_transform_resettable(dsl_state, %{resettable: resettable} = strategy) do
|
||||
with resettable <-
|
||||
maybe_set_field_lazy(
|
||||
resettable,
|
||||
|
@ -378,21 +384,16 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
|||
resettable.password_reset_action_name,
|
||||
&build_reset_action(&1, resettable, strategy)
|
||||
),
|
||||
:ok <- validate_reset_action(dsl_state, resettable, strategy) do
|
||||
{:ok, dsl_state, %{strategy | resettable: [resettable]}}
|
||||
resettable <- maybe_transform_token_lifetime(resettable, :token_lifetime, :hours),
|
||||
:ok <-
|
||||
validate_reset_action(dsl_state, resettable, strategy) do
|
||||
{:ok, dsl_state, %{strategy | resettable: resettable}}
|
||||
else
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_transform_resettable(_dsl_state, %{resettable: [_ | _]}),
|
||||
do:
|
||||
DslError.exception(
|
||||
path: [:authentication, :strategies, :password],
|
||||
message: "Only one `resettable` entity may be present."
|
||||
)
|
||||
|
||||
defp build_reset_request_action(dsl_state, resettable, strategy) do
|
||||
identity_attribute = Resource.Info.attribute(dsl_state, strategy.identity_field)
|
||||
|
||||
|
|
|
@ -69,7 +69,8 @@ defmodule AshAuthentication.Strategy.Password.Verifier do
|
|||
|
||||
defp validate_tokens_enabled_for_sign_in_tokens(_, _), do: :ok
|
||||
|
||||
defp maybe_validate_resettable_sender(dsl_state, %{resettable: [resettable]}) do
|
||||
defp maybe_validate_resettable_sender(dsl_state, %{resettable: resettable})
|
||||
when is_struct(resettable) do
|
||||
with {:ok, {sender, _opts}} <- Map.fetch(resettable, :sender),
|
||||
:ok <- validate_behaviour(sender, Sender) do
|
||||
:ok
|
||||
|
|
|
@ -33,6 +33,7 @@ defmodule AshAuthentication.Transformer do
|
|||
with :ok <- validate_at_least_one_strategy(dsl_state),
|
||||
:ok <- validate_unique_strategy_names(dsl_state),
|
||||
:ok <- validate_unique_add_on_names(dsl_state),
|
||||
{:ok, dsl_state} <- maybe_transform_token_lifetime(dsl_state),
|
||||
{:ok, get_by_subject_action_name} <-
|
||||
Info.authentication_get_by_subject_action_name(dsl_state),
|
||||
{:ok, dsl_state} <-
|
||||
|
@ -52,6 +53,29 @@ defmodule AshAuthentication.Transformer do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_transform_token_lifetime(dsl_state) do
|
||||
case Info.authentication_tokens_token_lifetime(dsl_state) do
|
||||
{:ok, {_ttl, unit}} when unit in ~w[days hours minutes seconds]a ->
|
||||
{:ok, dsl_state}
|
||||
|
||||
{:ok, ttl} when is_integer(ttl) and ttl > 0 ->
|
||||
{:ok,
|
||||
Transformer.set_option(
|
||||
dsl_state,
|
||||
[:authentication, :tokens],
|
||||
:token_lifetime,
|
||||
{ttl, :hours}
|
||||
)}
|
||||
|
||||
_ ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
path: [:authentication, :tokens],
|
||||
message: "Invalid token lifetime"
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_get_by_subject_action(dsl_state) do
|
||||
with {:ok, get_by_subject_action_name} <-
|
||||
Info.authentication_get_by_subject_action_name(dsl_state) do
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -16,6 +16,7 @@ defmodule AshAuthentication.MixProject do
|
|||
deps: deps(),
|
||||
package: package(),
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
consolidate_protocols: Mix.env() == :prod,
|
||||
dialyzer: [
|
||||
plt_add_apps: [:mix, :ex_unit],
|
||||
plt_core_path: "priv/plts",
|
||||
|
|
|
@ -209,7 +209,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
|
|||
params = %{"username" => user.username}
|
||||
options = []
|
||||
api = Info.authentication_api!(strategy.resource)
|
||||
resettable = strategy.resettable |> Enum.at(0)
|
||||
resettable = strategy.resettable
|
||||
|
||||
result =
|
||||
strategy.resource
|
||||
|
@ -246,7 +246,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
|
|||
|
||||
test "it returns an error when the strategy is not resettable" do
|
||||
{:ok, strategy} = Info.strategy(Example.User, :password)
|
||||
strategy = %{strategy | resettable: []}
|
||||
strategy = %{strategy | resettable: nil}
|
||||
|
||||
assert {:error, error} = Actions.reset_request(strategy, %{"username" => username()}, [])
|
||||
assert Exception.message(error) =~ ~r/no such action/i
|
||||
|
|
|
@ -14,7 +14,7 @@ defmodule AshAuthentication.Strategy.Password.StrategyTest do
|
|||
|
||||
describe "Strategy.phases/1" do
|
||||
test "it returns the correct phases when the strategy supports resetting" do
|
||||
strategy = %Password{resettable: [%Resettable{}]}
|
||||
strategy = %Password{resettable: %Resettable{}}
|
||||
|
||||
phases =
|
||||
strategy
|
||||
|
@ -38,7 +38,7 @@ defmodule AshAuthentication.Strategy.Password.StrategyTest do
|
|||
|
||||
describe "Strategy.actions/1" do
|
||||
test "it returns the correct actions when the strategy supports resetting" do
|
||||
strategy = %Password{resettable: [%Resettable{}]}
|
||||
strategy = %Password{resettable: %Resettable{}}
|
||||
|
||||
actions =
|
||||
strategy
|
||||
|
@ -95,7 +95,7 @@ defmodule AshAuthentication.Strategy.Password.StrategyTest do
|
|||
{:ok, strategy} = Info.strategy(Example.User, :password)
|
||||
|
||||
routes =
|
||||
%{strategy | resettable: []}
|
||||
%{strategy | resettable: nil}
|
||||
|> Strategy.routes()
|
||||
|> MapSet.new()
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ defmodule AshAuthentication.Strategy.PasswordTest do
|
|||
describe "reset_token_for/1" do
|
||||
test "it generates a token when resets are enabled" do
|
||||
user = build_user()
|
||||
resettable = %Resettable{password_reset_action_name: :reset, token_lifetime: 72}
|
||||
strategy = %Password{resettable: [resettable], resource: user.__struct__}
|
||||
resettable = %Resettable{password_reset_action_name: :reset, token_lifetime: {72, :hours}}
|
||||
strategy = %Password{resettable: resettable, resource: user.__struct__}
|
||||
|
||||
assert {:ok, token} = Password.reset_token_for(strategy, user)
|
||||
assert {:ok, claims} = Jwt.peek(token)
|
||||
|
|
Loading…
Reference in a new issue