<!--
SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
SPDX-License-Identifier: MIT
-->
# Building a password-change UI
AshAuthentication's Igniter task adds a `:change_password` action when you specify the password strategy, but AshAuthenticationPhoenix does not provide a component for this action, so you will need to either write your own, or use one provided by a component library that supports [AshPhoenix](https://hexdocs.pm/ash_phoenix/). The main reason for this is that the password-change UI is usually not as separate from the rest of an application as sign-in, registration, and password-reset actions.
This is the `:change_password` action that we are starting with, generated by Igniter.
```elixir
# lib/my_app/accounts/user.ex
# ...
update :change_password do
require_atomic? false
accept []
argument :current_password, :string, sensitive?: true, allow_nil?: false
argument :password, :string,
sensitive?: true,
allow_nil?: false,
constraints: [min_length: 8]
argument :password_confirmation, :string, sensitive?: true, allow_nil?: false
validate confirm(:password, :password_confirmation)
validate {AshAuthentication.Strategy.Password.PasswordValidation,
strategy_name: :password, password_argument: :current_password}
change {AshAuthentication.Strategy.Password.HashPasswordChange, strategy_name: :password}
end
# ...
```
## LiveComponent
Most web applications that you have used likely had the UI for changing your password somewhere under your personal settings. We are going to do the same, and create a [LiveComponent](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html) to contain our password-change UI, which we can then mount in a LiveView with the rest of the user settings.
Start by defining the module, and defining the template in the `render/1` function.
```elixir
# lib/my_app_web/components/change_password_component.ex
defmodule MyAppWeb.ChangePasswordComponent do
@moduledoc """
LiveComponent for changing the current user's password.
"""
use MyAppWeb, :live_component
alias MyApp.Accounts.User
@impl true
def render(assigns) do
~H"""
<div>
<.simple_form
for={@form}
id="user-password-change-form"
phx-target={@myself}
phx-submit="save"
>
<.input field={@form[:current_password]} type="password" label="Current Password" />
<.input field={@form[:password]} type="password" label="New Password" />
<.input field={@form[:password_confirmation]} type="password" label="Confirm New Password" />
<:actions>
<.button phx-disable-with="Saving...">Save</.button>
</:actions>
</.simple_form>
</div>
"""
end
end
```
This will produce a form with the usual three fields (current password, new password, and confirmation of new password, to guard against typographical errors), and a submit button. Now let's populate the `@form` assign.
```elixir
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_form()}
end
defp assign_form(%{assigns: %{current_user: user}} = socket) do
form = AshPhoenix.Form.for_update(user, :change_password, as: "user", actor: user)
assign(socket, form: to_form(form))
end
```
`update/2` is covered in the `Phoenix.LiveComponent` life-cycle documentation, so we won't go into it here. The private function `assign_form/1` should look familiar if you have any forms for Ash resources in your application, but with a significant addition: the `prepare_source` option.
The attribute `phx-target={@myself}` on the form in our template ensures the submit event is received by the component, so the `handle_event/3` function in this module is called, rather than the LiveView that mounts this component receiving the event.
```elixir
@impl true
def handle_event("save", %{"user" => user_params}, %{assigns: assigns} = socket) do
case AshPhoenix.Form.submit(assigns.form, params: user_params) do
{:ok, user} ->
assigns.on_saved.()
{:noreply, socket}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
```
Again, this should look familiar if you have any forms on your other application resources, but handle the success case a little differently here, calling the function passed by the parent LiveView via the `on_saved` attribute. This will make more sense when we use this component in a LiveView.
## Policies
Since the password-change workflow is done entirely in our application code, the [AshAuthentication policy bypass](https://hexdocs.pm/ash_authentication/policies-on-authentication-resources.html) will not pass. We need to add a policy that allows a user to run the `:change_password` action on themselves.
```elixir
# lib/my_app/accounts/user.ex
# ...
policies do
# ...
policy action(:change_password) do
description "Users can change their own password"
authorize_if expr(id == ^actor(:id))
end
end
# ...
```
## Using the LiveComponent
Finally, let's use this component in our UI somewhere. Exactly where this belongs will depend on the wider UX of your application, so for the sake of example, let's assume that you already have a LiveView called `LiveUserSettings` in your application, where you want to add the password-change form.
```elixir
defmodule MyAppWeb.LiveUserSettings do
@moduledoc """
LiveView for the current user's account settings.
"""
use MyAppWeb, :live_view
alias MyAppWeb.ChangePasswordComponent
@impl true
def render(assigns) do
~H"""
<.header>
Settings
</.header>
<% # ... %>
<.live_component
module={ChangePasswordComponent}
id="change-password-component"
current_user={@current_user}
on_saved={fn -> send(self(), {:saved, :password}) end}
/>
<% # ... %>
"""
end
@impl true
def handle_info({:saved, :password}, socket) do
{:noreply,
socket
|> put_flash(:info, "Password changed successfully")}
end
end
```
For the `on_saved` callback attribute mentioned earlier, we pass a function that sends a message to the process for this LiveView, and then write a `handle_info/2` clause that matches this message and which puts up an info flash informing the user that the password change succeeded. This interface decouples `ChangePasswordComponent` from where it is used. It manages only the password-change form, and leaves user feedback up to the parent. You could put the form in a modal that closes when the form submits successfully without having to change any code in the component, only `LiveUserSettings`.
## Security Email Notification
This gets you a working password-change UI, but you should also send an email notification to the user upon a password change. The reason for this is so that in the case of an account compromise, the attacker cannot change the password and lock out the rightful owner without alerting them.
The simplest way to do this is with a [notifier](https://hexdocs.pm/ash/notifiers.html).
```elixir
# ...
update :change_password do
notifiers [MyApp.Notifiers.EmailNotifier]
# ...
```
```elixir
# lib/my_app/notifiers/email_notifier.ex
defmodule MyApp.Notifiers.EmailNotifier do
use Ash.Notifier
alias MyApp.Accounts.User
@impl true
def notify(%Ash.Notifier.Notification{action: %{name: :change_password}, resource: user}) do
User.Email.deliver_password_change_notification(user)
end
end
```
```elixir
defmodule MyApp.Accounts.User.Email do
@moduledoc """
Email notifications for `User` records.
"""
def deliver_password_change_notification(user) do
{"""
<html>
<p>
Hi #{user.display_name},
</p>
<p>
Someone just changed your password. If this was not you,
please <a href="mailto:support@example.com">contact support</a>
<em>immediately</em>, because it means someone else has taken over
your account.
</p>
</html>
""",
"""
Hi #{user.display_name},
Someone just changed your password. If this was not you, please contact
support <support@example.net> <em>immediately</em>, because it means
someone else has taken over your account.
"""}
|> MyApp.Mailer.send_mail_to_user("Your password has been changed", user)
end
end
```
`MyApp.Mailer.send_mail_to_user/3` would be your application's internal interface to whichever mailer you are using, such as [Swoosh](https://hexdocs.pm/swoosh) or [Bamboo](https://hexdocs.pm/bamboo), that takes the HTML and text email bodies in a two-element tuple, the subject line, and the recipient user.
# Field Policies
If you are not using field policies, or you are using field policies with `private_fields :show` (the default), you can skip this section.
When using field policies, the `@current_user` assign set by AshAuthentication may not contain the value of the `hashed_password` attribute, because it is a private field (if you are using `private_fields :hide` or `private_fields :include`). This is what you normally want in your application, but the `:change_password` action needs this to validate the `:current_password` argument, you will need to explicitly load this attribute when creating the form in `ChangePasswordComponent`.
### Load `hashed_password` in the form
Change the function `assign_form/1` in `ChangePasswordComponent` as follows.
```elixir
defp assign_form(%{assigns: %{current_user: user}} = socket) do
form =
AshPhoenix.Form.for_update(user, :change_password,
as: "user",
# Add this argument
prepare_source: fn changeset ->
%{
changeset
| data:
Ash.load!(changeset.data, :hashed_password,
context: %{private: %{password_change?: true}}
)
}
end,
actor: user
)
assign(socket, form: to_form(form))
end
```
There are a couple of things going on here:
1. We are calling `Ash.load!/3` to load the attribute `hashed_password` on the record in the changeset.
2. Setting a private context field for this `Ash.load!/3` call to be used in a field policy bypass that we need to write in order for this load to succeed.
### Field Policy Bypass
The actual work will be done in a separate policy check module, so our bypass will look very simple. If you are using `private_fields :hide`, you will need to change it to `private_fields :include`, otherwise the `hashed_password` field will always be hidden, regardless of any field policy bypass.
```elixir
# lib/my_app/accounts/user.ex
# ...
field_policies do
private_fields :include
# ...
field_policy_bypass :* do
description "Users can access all fields for password change"
authorize_if MyApp.Checks.PasswordChangeInteraction
end
end
# ...
```
```elixir
# lib/my_app/checks/password_change_interaction.ex
defmodule MyApp.Checks.PasswordChangeInteraction do
use Ash.Policy.SimpleCheck
@impl Ash.Policy.Check
def describe(_) do
"MyApp is performing a password change for this interaction"
end
@impl Ash.Policy.SimpleCheck
def match?(_, %{subject: %{context: %{private: %{password_change?: true}}}}, _), do: true
def match?(_, _, _), do: false
end
```
This is actually how `AshAuthentication.Checks.AshAuthenticationInteraction` is implemented, only matching a slightly different pattern.