feat: support new sign in tokens feature on password strategy (#176)

As of version `3.10.5` `ash_authentication` now supports generating and validating short-lived sign in tokens as part of the password sign in flow.  This feature seamlessly enables this flow simply by turning on `sign_in_tokens_enabled?` in the password strategy DSL.

---------

Co-authored-by: James Harton <james@harton.nz>
This commit is contained in:
Zach Daniel 2023-04-05 23:59:52 -04:00 committed by GitHub
parent 2ce0430e8c
commit 903f3a386e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 82 additions and 20 deletions

View file

@ -19,8 +19,7 @@ defmodule DevWeb.AuthController do
def failure(conn, _activity, reason) do
conn
|> assign(:failure_reason, reason)
|> put_status(401)
|> render("failure.html")
|> redirect(to: "/sign-in")
end
@doc false

View file

@ -224,15 +224,16 @@ defmodule Example.Accounts.User do
strategies do
password :password do
identity_field(:email)
identity_field :email
sign_in_tokens_enabled? true
end
end
tokens do
enabled?(true)
token_resource(Example.Accounts.Token)
enabled? true
token_resource Example.Accounts.Token
signing_secret(Application.compile_env(:example, ExampleWeb.Endpoint)[:secret_key_base])
signing_secret Application.compile_env(:example, ExampleWeb.Endpoint)[:secret_key_base]
end
end

View file

@ -0,0 +1,19 @@
defimpl AshPhoenix.FormData.Error, for: AshAuthentication.Errors.AuthenticationFailed do
import Phoenix.HTML.Form, only: [humanize: 1]
def to_form_error(error) when is_struct(error.strategy, AshAuthentication.Strategy.Password) do
[
{error.strategy.password_field,
"#{humanize(error.strategy.identity_field)} or #{downcase_humanize(error.strategy.password_field)} was incorrect",
[]}
]
end
def to_form_error(_), do: []
defp downcase_humanize(value) do
value
|> humanize()
|> String.downcase()
end
end

View file

@ -67,6 +67,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
```
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""

View file

@ -286,7 +286,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.Input do
<ul class={override_for(@overrides, :error_ul)}>
<%= for error <- @errors do %>
<li class={override_for(@overrides, :error_li)} phx-feedback-for={input_name(@form, @field)}>
<%= @field_label %> <%= error %>
<%= error %>
</li>
<% end %>
</ul>

View file

@ -35,7 +35,10 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
alias AshAuthentication.{Info, Phoenix.Components.Password, Strategy}
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers, only: [route_helpers: 1]
import AshAuthentication.Phoenix.Components.Helpers,
only: [route_helpers: 1]
import Phoenix.HTML.Form
import Slug
@ -139,14 +142,40 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
def handle_event("submit", params, socket) do
params = get_params(params, socket.assigns.strategy)
form = Form.validate(socket.assigns.form, params)
socket =
socket
|> assign(:form, form)
|> assign(:trigger_action, form.valid?)
if socket.assigns.strategy.sign_in_tokens_enabled? do
case Form.submit(socket.assigns.form,
params: params,
read_one?: true,
before_submit: fn changeset ->
Ash.Changeset.set_context(changeset, %{token_type: :sign_in})
end
) do
{:ok, user} ->
validate_sign_in_token_path =
route_helpers(socket).auth_path(
socket.endpoint,
{socket.assigns.subject_name, Strategy.name(socket.assigns.strategy),
:sign_in_with_token},
token: user.__metadata__.token
)
{:noreply, socket}
{:noreply, redirect(socket, to: validate_sign_in_token_path)}
{:error, form} ->
{:noreply,
assign(socket, :form, Form.clear_value(form, socket.assigns.strategy.password_field))}
end
else
form = Form.validate(socket.assigns.form, params)
socket =
socket
|> assign(:form, form)
|> assign(:trigger_action, form.valid?)
{:noreply, socket}
end
end
defp get_params(params, strategy) do

View file

@ -47,7 +47,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
required(:strategy) => AshAuthentication.Strategy.t(),
required(:token) => String.t(),
optional(:label) => String.t() | false,
optional(:overrices) => [module]
optional(:overrides) => [module]
}
@doc false

View file

@ -2,7 +2,10 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
strategy_class: "CSS class for a `div` surrounding each strategy component.",
show_banner: "Whether or not to show the banner."
show_banner: "Whether or not to show the banner.",
authentication_error_container_class:
"CSS class for the container for the text of the authentication error.",
authentication_error_text_class: "CSS class for the authentication error text."
@moduledoc """
Renders sign in mark-up for an authenticated resource.

View file

@ -109,6 +109,7 @@ defmodule AshAuthentication.Phoenix.Controller do
import Phoenix.Controller
import Plug.Conn
import AshAuthentication.Phoenix.Plug
import AshAuthentication.Phoenix.Controller
@doc false
@impl true

View file

@ -40,6 +40,9 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
"""
set :strategy_class, "mx-auth w-full max-w-sm lg:w-96"
set :authentication_error_container_class, "text-black dark:text-white text-center"
set :authentication_error_text_class, ""
end
override Components.Banner do
@ -129,6 +132,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
set :input_class_with_error, """
appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md
shadow-sm placeholder-gray-400 focus:outline-none border-red-400 sm:text-sm
dark:text-black
"""
set :submit_class, """

View file

@ -113,7 +113,7 @@ defmodule AshAuthentication.Phoenix.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash_authentication, "~> 3.5"},
{:ash_authentication, "~> 3.10"},
{:ash_phoenix, "~> 1.1"},
{:ash, "~> 2.2"},
{:jason, "~> 1.0"},

View file

@ -1,6 +1,6 @@
%{
"ash": {:hex, :ash, "2.6.29", "ab5aee1a0da3d3a5f3f190aadc524da961baf23691e9243dfcd9bf29a3e33555", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, "~> 1.0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f25f18e3fc9e698732c068b7f5262a21d125a9824619ffd771dbc0d997e8206"},
"ash_authentication": {:hex, :ash_authentication, "3.10.4", "c1649865160904aaf8c23ab0bbfed22bbc2c7192ac89c47ce5f22eedd5f66aa4", [:mix], [{:ash, "~> 2.5 and >= 2.5.11", [hex: :ash, repo: "hexpm", optional: false]}, {:assent, "~> 0.2", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:mint, "~> 1.4", [hex: :mint, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 0.4 and >= 0.4.1 or ~> 1.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "6e396994085da4d17c0a89ef85a6550792522394f3553c98a7de8deb0dfd8e1c"},
"ash_authentication": {:hex, :ash_authentication, "3.10.5", "8a5e9b4b6887c8f6ddd44763dd1ce11fd6db1376e11cfa90dbbc24a72ee2ab2b", [:mix], [{:ash, ">= 2.5.11 and < 3.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:assent, "~> 0.2", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:mint, "~> 1.4", [hex: :mint, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, ">= 0.4.1 and < 1.0.0-0 or ~> 1.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "6a7d5a64ce8afed4d13231b3964e79870c3de5d1d33b7311eb5173d9a64ceef0"},
"ash_phoenix": {:hex, :ash_phoenix, "1.2.12", "83e32d0b192b6815ba2b2766448508cebd957b4a08a96a4273ed41dfb9afcbdb", [:mix], [{:ash, "~> 2.5 and >= 2.5.10", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "05f72de031a723d052e85cef8cdff3972e855b53dd5daa3787d13c07cbe017f7"},
"assent": {:hex, :assent, "0.2.3", "414d77ea27349dacc980b612e9edeed06c4d64a3df99a0fa8e42e6940ed20c16", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "a39bc5b57920632b003bd175fd58fcb355c10efbe614bba03682ce2a76d4133f"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
@ -50,7 +50,7 @@
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
"sourceror": {:hex, :sourceror, "0.12.2", "2ae55efd149193572e0eb723df7c7a1bda9ab33c43373c82642931dbb2f4e428", [:mix], [], "hexpm", "7ad74ade6fb079c71f29fae10c34bcf2323542d8c51ee1bcd77a546cfa89d59c"},
"spark": {:hex, :spark, "1.0.3", "bd31519fdb68247556372e1167bbf3b1db300cde964064975129c0555eb6ae7c", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "5da2c9bc6b1d197be887f3f162316df7ba3227001c31471087bca11d50991bff"},
"spark": {:hex, :spark, "1.0.4", "973d9c02fd4a87ca1de89047521fb479ea7d9acd59be4c14afef0aa28e9c2cab", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "56b6d721f458bb683ead7d8870ec6cdabc8a8191feeb3f59357e6ff2adce2907"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},

View file

@ -7,4 +7,8 @@ defmodule Example.Accounts.Token do
token do
api Example.Accounts
end
actions do
defaults [:read]
end
end

View file

@ -78,7 +78,8 @@ defmodule Example.Accounts.User do
password do
identity_field(:email)
hashed_password_field(:hashed_password)
registration_enabled? false
registration_enabled? true
sign_in_tokens_enabled? true
resettable do
sender(fn user, token, _ ->