PhoenixLiveViewを使用したmix phx.gen_authを詳しく見てみた【登録〜認証編】①

# はじめに

この記事ではPhoenixのバージョン`1.7.0`を使用しています。

```bash
$ mix phx.new -v
Phoenix installer v1.7.0
```

またこの記事ではアカウントの登録から認証までを見ていきます。

以下はこのシリーズの目次です。

- PhoenixLiveViewを使用したmix phx.gen_authを詳しく見てみた【登録〜認証編】①
- [PhoenixLiveViewを使用したmix phx.gen_authを詳しく見てみた【認証メール再送信編】②](https://qiita.com/suzuki-t0505/items/28bf11f978fcb7a85adb)
- [PhoenixLiveViewを使用したmix phx.gen_authを詳しく見てみた【ログイン編】③](https://qiita.com/suzuki-t0505/items/207660d202d693502cdd)

## 目次

- [はじめに](#はじめに)
- [プロジェクトの作成](#プロジェクトの作成)
- [phx.gen.authの実行](#phx.gen.authの実行)
- [生成されたファイルの確認](#生成されたファイルの確認)
    - [アカウント登録](#アカウント登録)
    - [2要素認証](#2要素認証)
- [終わり](#終わり)

## プロジェクトの作成

まず、プロジェクトの作成をします。

```bash
$ mix phx.new auth_sample
$ ce auth_sample
$ mix ecto.create
```

## phx.gen.authの実行


途中で「LiveViewベースの認証システムを作成しますか?」と聞かれるので「y」を入力します。

```bash
$ mix phx.gen.auth Accounts Account accounts
An authentication system can be created in two different ways:
- Using Phoenix.LiveView (default)
- Using Phoenix.Controller only
Do you want to create a LiveView based authentication system? [Yn] y
* creating priv/repo/migrations/20230301141102_create_accounts_auth_tables.exs
* creating lib/auth_sample/accounts/account_notifier.ex
* creating lib/auth_sample/accounts/account.ex
* creating lib/auth_sample/accounts/account_token.ex
* creating lib/auth_sample_web/account_auth.ex
* creating test/auth_sample_web/account_auth_test.exs
* creating lib/auth_sample_web/controllers/account_session_controller.ex
* creating test/auth_sample_web/controllers/account_session_controller_test.exs
* creating lib/auth_sample_web/live/account_registration_live.ex
* creating test/auth_sample_web/live/account_registration_live_test.exs
* creating lib/auth_sample_web/live/account_login_live.ex
* creating test/auth_sample_web/live/account_login_live_test.exs
* creating lib/auth_sample_web/live/account_reset_password_live.ex
* creating test/auth_sample_web/live/account_reset_password_live_test.exs
* creating lib/auth_sample_web/live/account_forgot_password_live.ex
* creating test/auth_sample_web/live/account_forgot_password_live_test.exs
* creating lib/auth_sample_web/live/account_settings_live.ex
* creating test/auth_sample_web/live/account_settings_live_test.exs
* creating lib/auth_sample_web/live/account_confirmation_live.ex
* creating test/auth_sample_web/live/account_confirmation_live_test.exs
* creating lib/auth_sample_web/live/account_confirmation_instructions_live.ex
* creating test/auth_sample_web/live/account_confirmation_instructions_live_test.exs
* creating lib/auth_sample/accounts.ex
* injecting lib/auth_sample/accounts.ex
* creating test/auth_sample/accounts_test.exs
* injecting test/auth_sample/accounts_test.exs
* creating test/support/fixtures/accounts_fixtures.ex
* injecting test/support/fixtures/accounts_fixtures.ex
* injecting test/support/conn_case.ex
* injecting config/test.exs
* injecting mix.exs
* injecting lib/auth_sample_web/router.ex
* injecting lib/auth_sample_web/router.ex - imports
* injecting lib/auth_sample_web/router.ex - plug
* injecting lib/auth_sample_web/components/layouts/root.html.heex

Please re-fetch your dependencies with the following command:

    $ mix deps.get

Remember to update your repository by running migrations:

    $ mix ecto.migrate

Once you are ready, visit "/accounts/register"
to create your account and then access "/dev/mailbox" to
see the account confirmation email.
```
依存関係の取得をします。

```bash
$ mix deps.get
```

マイグレートを実行してテーブルを作成します。

```bash
$ mix ecto.migrate
```

## 生成されたファイルの確認

`mix phx.gen.auth`で生成されたファイルをアカウントの登録、認証の順で確認します。

### アカウント登録

まずアカウント登録を確認します。

[http://localhost:4000/accounts/register](http://localhost:4000/accounts/register)

![Screenshot from 2023-03-01 23-30-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646507/9ccff0c0-a779-2e15-6f85-666b691253e5.png)

アカウント登録に該当するソースコードは以下です。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
defmodule AuthSampleWeb.AccountRegistrationLive do
  use AuthSampleWeb, :live_view

  alias AuthSample.Accounts
  alias AuthSample.Accounts.Account

  def render(assigns) do
    ~H"""
    <div class="mx-auto max-w-sm">
      <.header class="text-center">
        Register for an account
        <:subtitle>
          Already registered?
          <.link navigate={~p"/accounts/log_in"} class="font-semibold text-brand hover:underline">
            Sign in
          </.link>
          to your account now.
        </:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="registration_form"
        phx-submit="save"
        phx-change="validate"
        phx-trigger-action={@trigger_submit}
        action={~p"/accounts/log_in?_action=registered"}
        method="post"
      >
        <.error :if={@check_errors}>
          Oops, something went wrong! Please check the errors below.
        </.error>

        <.input field={@form[:email]} type="email" label="Email" required />
        <.input field={@form[:password]} type="password" label="Password" required />

        <:actions>
          <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    changeset = Accounts.change_account_registration(%Account{})

    socket =
      socket
      |> assign(trigger_submit: false, check_errors: false)
      |> assign_form(changeset)

    {:ok, socket, temporary_assigns: [form: nil]}
  end

  def handle_event("save", %{"account" => account_params}, socket) do
    case Accounts.register_account(account_params) do
      {:ok, account} ->
        {:ok, _} =
          Accounts.deliver_account_confirmation_instructions(
            account,
            &url(~p"/accounts/confirm/#{&1}")
          )

        changeset = Accounts.change_account_registration(account)
        {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
    end
  end

  def handle_event("validate", %{"account" => account_params}, socket) do
    changeset = Accounts.change_account_registration(%Account{}, account_params)
    {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
  end

  defp assign_form(socket, %Ecto.Changeset{} = changeset) do
    form = to_form(changeset, as: "account")

    if changeset.valid? do
      assign(socket, form: form, check_errors: false)
    else
      assign(socket, form: form)
    end
  end
end
```

上から順に見ていきます。

まずは`mount`

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
def mount(_params, _session, socket) do
  changeset = Accounts.change_account_registration(%Account{})

  socket =
    socket
    |> assign(trigger_submit: false, check_errors: false)
    |> assign_form(changeset)

  {:ok, socket, temporary_assigns: [form: nil]}
end
# ・・・（省略）
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
  form = to_form(changeset, as: "account")

  if changeset.valid? do
    assign(socket, form: form, check_errors: false)
  else
    assign(socket, form: form)
  end
end
```

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
changeset = Accounts.change_account_registration(%Account{})
```

上記は`Accounts.Account`のチェンジセットを作成する関数です。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
form = to_form(changeset, as: "account")
```

上記は`Phoenix.HTML.Form`構造体に引数で渡されたデータを変換する関数です。`to_form/2`は、マップまたはEctoチェンジセットをコンポーネントに渡すために、フォームに変換します。
第2引数で渡してる`as: "account"`の`as`オプションはフォーム入力で使用されるプレフィックスです。

`Phoenix.HTML.Form`構造体に変換されたデータは以下です。

```elixir
%Phoenix.HTML.Form{
  source: #Ecto.Changeset<
    action: nil,
    changes: %{},
    errors: [
      password: {"can't be blank", [validation: :required]},
      email: {"can't be blank", [validation: :required]}
    ],
    data: #AuthSample.Accounts.Account<>,
    valid?: false
  >,
  impl: Phoenix.HTML.FormData.Ecto.Changeset,
  id: "account",
  name: "account",
  data: #AuthSample.Accounts.Account<
    __meta__: #Ecto.Schema.Metadata<:built, "accounts">,
    id: nil,
    email: nil,
    confirmed_at: nil,
    inserted_at: nil,
    updated_at: nil,
    ...
  >,
  hidden: [],
  params: %{},
  errors: [],
  options: [method: "post"],
  index: nil,
  action: nil
}
```

少しわかりやすくします。これはあくまでも見やすくしている記述しているため、実際には変更はしていません。

```elixir
def mount(_params, _session, socket) do
  form = 
    %Account{}
    |> Accounts.change_account_registration()
    |> to_form(as: "account")

  socket =
    socket
    |> assign(trigger_submit: false, check_errors: false)
    |> assign(form: form, check_errors: false)

  {:ok, socket, temporary_assigns: [form: nil]}
end
```

ここまでで、どのような値がアサインされているかわかりました。

では次に`render`の部分を見ていきます。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
def render(assigns) do
  ~H"""
  <div class="mx-auto max-w-sm">
    <.header class="text-center">
      Register for an account
      <:subtitle>
        Already registered?
        <.link navigate={~p"/accounts/log_in"} class="font-semibold text-brand hover:underline">
          Sign in
        </.link>
        to your account now.
      </:subtitle>
    </.header>

    <.simple_form
      for={@form}
      id="registration_form"
      phx-submit="save"
      phx-change="validate"
      phx-trigger-action={@trigger_submit}
      action={~p"/accounts/log_in?_action=registered"}
      method="post"
    >
      <.error :if={@check_errors}>
        Oops, something went wrong! Please check the errors below.
      </.error>

      <.input field={@form[:email]} type="email" label="Email" required />
      <.input field={@form[:password]} type="password" label="Password" required />

      <:actions>
        <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
      </:actions>
    </.simple_form>
  </div>
  """
end
```

ここではコンポーネントについては省略します。

次にフォームに入力している時に呼ぶイベント`"validate"`を確認します。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
def handle_event("validate", %{"account" => account_params}, socket) do
  changeset = Accounts.change_account_registration(%Account{}, account_params)
  {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
end
```

特に複雑な処理は無く、フォームに入力があったらこのイベントが呼ばれ、チェンジセットを入力した値に更新し続ける（フォーム入力中だけ）処理です。


次にフォームに値を入力して「Create an account」ボタンを押した際に呼ぶイベント`"save"`を確認します。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
def handle_event("save", %{"account" => account_params}, socket) do
  case Accounts.register_account(account_params) do
    {:ok, account} ->
      {:ok, _} =
        Accounts.deliver_account_confirmation_instructions(
          account,
          &url(~p"/accounts/confirm/#{&1}")
        )

      changeset = Accounts.change_account_registration(account)
      {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
  end
end
```

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
Accounts.register_account(account_params)
```

上記はフォームで入力された値を検証（チェンジセットを作成）し`accounts`テーブルにデータを挿入している関数です。
データの挿入に失敗した際はチェンジセットを受け取り、フォームにエラーを表示させます。
挿入に成功した場合は次にメール送信のステップに移行します。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
Accounts.deliver_account_confirmation_instructions(account, &url(~p"/accounts/confirm/#{&1}"))
```

上記の関数はトークンを生成し、メールを送信する関数です。中の処理を確認します。

```elixir:auth_sample/lib/auth_sample/accounts.ex
def deliver_account_confirmation_instructions(%Account{} = account, confirmation_url_fun)
    when is_function(confirmation_url_fun, 1) do
  if account.confirmed_at do
    {:error, :already_confirmed}
  else
    {encoded_token, account_token} = AccountToken.build_email_token(account, "confirm")
    Repo.insert!(account_token)
    AccountNotifier.deliver_confirmation_instructions(account, confirmation_url_fun.(encoded_token))
  end
end
```

上記の関数が呼ばれます。
まず`if`マクロで`account.confirmed_at`がtrueかfalseかを確認しています。
※elixirでのfalseは`false`または`nil`です。それ以外はすべて`true`として扱われます。
ここでは、このアカウントが2要素認証がすでに済んでいる場合は`{:error, :already_confirmed}が返され、メールは送信されません。

```elixir:auth_sample/lib/auth_sample/accounts.ex
{encoded_token, account_token} = AccountToken.build_email_token(account, "confirm")
```

2要素認証が済んでいない場合はまずアカウント登録用のトークンを生成するためのクエリとエンコードされたトークンを作成します。
こちらの中も確認します。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
@hash_algorithm :sha256
@rand_size 32
def build_email_token(account, context) do
  build_hashed_token(account, context, account.email)
end

defp build_hashed_token(account, context, sent_to) do
  token = :crypto.strong_rand_bytes(@rand_size)
  hashed_token = :crypto.hash(@hash_algorithm, token)

  {Base.url_encode64(token, padding: false),
   %AccountToken{
    token: hashed_token,
    context: context,
    sent_to: sent_to,
    account_id: account.id
  }}
end
```

まず、バイトサイズが32のバイナリーを作成します。そのバイナリーを`sha256`でハッシュ化します。
次に`token`変数のバイナリーをBase64でエンコードした文字列を作成します。
`AccountToken`構造体にはそれぞれ値を割り当てます。
`:token`は`sha256`でハッシュ化したトークン
`:context`は引数で渡された`"confirm"`
`:sent_to`はアカウントのメールアドレス
`:account_id`はアカウントのid

エンコードされた値と`AccountToken`構造体をタプルの形にして返却しています。

その次に

```elixir:auth_sample/lib/auth_sample/accounts.ex
Repo.insert!(account_token)
AccountNotifier.deliver_confirmation_instructions(account, confirmation_url_fun.(encoded_token))
```

受け取った`AccountToken`構造体を`account_token`テーブルに挿入します。
その次にメール送信を行います。
その前に`AccountNotifier.deliver_confirmation_instructions/2`関数の第2引数は無名関数で作成したurlです。
処理をわかりやすくするしたものは以下です。

```elixir:auth_sample/lib/auth_sample/accounts.ex
def deliver_account_confirmation_instructions(%Account{} = account, confirmation_url_fun)
    when is_function(confirmation_url_fun, 1) do
  if account.confirmed_at do
    {:error, :already_confirmed}
  else
    {encoded_token, account_token} = AccountToken.build_email_token(account, "confirm")
    Repo.insert!(account_token)
    # url = url(~p"/accounts/confirm/#{encoded_token}") # 下記の実際の処理
    url = confirmation_url_fun.(encoded_token) # http://localhost:4000/accounts/confirm/xxxxxxxx xのところにエンコードされたトークンの文字列が入ります。
    AccountNotifier.deliver_confirmation_instructions(account, url)
  end
end
```

ここまでの処理で登録されたアカウントのメールアドレス宛に2要素認証のurlを含めたメールが送信されます。

ではliveに戻って次の処理を確認しましょう。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
changeset = Accounts.change_account_registration(account)
{:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
```

もう一度アカウントのチェンジセットを作成しています。
ここで注目するのは`assign(trigger_submit: true)`です。
`trigger_submit`はLiveViewのフォームバインディングです。
どのような属性かというと、フォームの`action`属性で指定されたURLへフォームを送信することができます。
どういうことかというと`trigger_submit`が`true`の時は、`phx-submit`で指定したアクションを呼ばずにLiveViewを切断して`action`属性で指定したURLへフォームを送信します。`trigger_submit`が`false`の時は`phx-submit`で指定したアクションを呼びます。
では実際にどのように使用するかはもう一度`"save"`アクションの関数を確認します。

```elixir:auth_sample/lib/auth_sample_web/live/account_registration_live.ex
def handle_event("save", %{"account" => account_params}, socket) do
  case Accounts.register_account(account_params) do
    {:ok, account} ->
      {:ok, _} =
        Accounts.deliver_account_confirmation_instructions(
          account,
          &url(~p"/accounts/confirm/#{&1}")
        )

      changeset = Accounts.change_account_registration(account)
      {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
  end
end
```

アカウントの挿入が正常に行われた場合は`trigger_submit`を`true`にしています。挿入に問題があった場合は`trigger_submit`は`false`のままでsocketを返しています。
このように`trigger_submit`はLiveViewフォームの送信前の検証を実行する際に使います。

LiveViewはsocket通信を使用するのでセッションに新しく追加するなどの操作ができないので、フォームをコントローラーのアクションに渡してセッションを追加するために`trigger_submit`を使用しているんだと思います。

`trigger_submit`が`true`になった状態でフォームの送信を受け取るコントローラーのアクションは以下です。

```elixir:auth_sample/lib/auth_sample_web/controllers/account_settion_controller.ex
defmodule AuthSampleWeb.AccountSessionController do
  use AuthSampleWeb, :controller

  alias AuthSample.Accounts
  alias AuthSampleWeb.AccountAuth

  def create(conn, %{"_action" => "registered"} = params) do
    create(conn, params, "Account created successfully!")
  end

# ・・・（省略）

  defp create(conn, %{"account" => account_params}, info) do
    %{"email" => email, "password" => password} = account_params

    if account = Accounts.get_account_by_email_and_password(email, password) do
      conn
      |> put_flash(:info, info)
      |> AccountAuth.log_in_account(account, account_params)
    else
      # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
      conn
      |> put_flash(:error, "Invalid email or password")
      |> put_flash(:email, String.slice(email, 0, 160))
      |> redirect(to: ~p"/accounts/log_in")
    end
  end
# ・・・（省略）
end
```

こちらは以前のgen_authとほぼ同じです。

受け取ったメールアドレスとパスワードからアカウントを取得できればログイン、できなければ、ログイン画面にリダイレクトするようになっています。

`AccountAuth.log_in_account(account, account_params)`の関数を見ていきましょう。

```elixir:auth_sample/lib/auth_sample_web/account_auth.ex
@max_age 60 * 60 * 24 * 60
@remember_me_cookie "_auth_sample_web_account_remember_me"
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]

def log_in_account(conn, account, params \\ %{}) do
  token = Accounts.generate_account_session_token(account)
  account_return_to = get_session(conn, :account_return_to)

  conn
  |> renew_session()
  |> put_token_in_session(token)
  |> maybe_write_remember_me_cookie(token, params)
  |> redirect(to: account_return_to || signed_in_path(conn))
end

defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
  put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end

defp maybe_write_remember_me_cookie(conn, _token, _params) do
  conn
end

# ・・・（省略）

defp renew_session(conn) do
  conn
  |> configure_session(renew: true)
  |> clear_session()
end

# ・・・（省略）

defp put_token_in_session(conn, token) do
  conn
  |> put_session(:account_token, token)
  |> put_session(:live_socket_id, "accounts_sessions:#{Base.url_encode64(token)}")
end

# ・・・（省略）

defp signed_in_path(_conn), do: ~p"/"
```

ここで行っている処理は、まずアカウントのログイン用トークンを生成します。
次にsessionにトークを追加します。
最後にsessionには`account_return_to`は追加されていないので`http://localhost:4000`にリダイレクトします。





ではフォームにメールアドレスとパスワードを入力してフォームを送信します。
送信して正常にアカウントが作成されるとホームにリダイレクトします。
![Screenshot from 2023-03-02 01-45-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646507/357eeeb2-b279-44a9-92bc-44f5b074fd99.png)


### 2要素認証

続いて2要素認証について確認します。

まず以下のurlにアクセスして先程送信されたメールを確認します。

[http://localhost:4000/dev/mailbox](http://localhost:4000/dev/mailbox)

アクセスして一番上にあるメールを開くと2要素認証のurlが確認できます。

![Screenshot from 2023-03-02 01-47-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646507/982701e8-66bf-37ad-b29d-9b1b12febfc4.png)

ここに記載されている`http://localhost:4000/accounts/confirm/xxxxxxxx`にアクセスします。

![Screenshot from 2023-03-02 16-29-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646507/6cbaf0b8-51f1-b53f-7b31-a7f5a36235d3.png)

2要素認証のソースコードは以下です。

```elixir:auth_sample/lib/auth_sample_web/live/account_confirmation_live.ex
defmodule AuthSampleWeb.AccountConfirmationLive do
  use AuthSampleWeb, :live_view

  alias AuthSample.Accounts

  def render(%{live_action: :edit} = assigns) do
    ~H"""
    <div class="mx-auto max-w-sm">
      <.header class="text-center">Confirm Account</.header>

      <.simple_form for={@form} id="confirmation_form" phx-submit="confirm_account">
        <.input field={@form[:token]} type="hidden" />
        <:actions>
          <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
        </:actions>
      </.simple_form>

      <p class="text-center mt-4">
        <.link href={~p"/accounts/register"}>Register</.link>
        |
        <.link href={~p"/accounts/log_in"}>Log in</.link>
      </p>
    </div>
    """
  end

  def mount(%{"token" => token}, _session, socket) do
    form = to_form(%{"token" => token}, as: "account")
    {:ok, assign(socket, form: form), temporary_assigns: [form: nil]}
  end

  # Do not log in the account after confirmation to avoid a
  # leaked token giving the account access to the account.
  def handle_event("confirm_account", %{"account" => %{"token" => token}}, socket) do
    case Accounts.confirm_account(token) do
      {:ok, _} ->
        {:noreply,
         socket
         |> put_flash(:info, "Account confirmed successfully.")
         |> redirect(to: ~p"/")}

      :error ->
        # If there is a current account and the account was already confirmed,
        # then odds are that the confirmation link was already visited, either
        # by some automation or by the account themselves, so we redirect without
        # a warning message.
        case socket.assigns do
          %{current_account: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
            {:noreply, redirect(socket, to: ~p"/")}

          %{} ->
            {:noreply,
             socket
             |> put_flash(:error, "Account confirmation link is invalid or it has expired.")
             |> redirect(to: ~p"/")}
        end
    end
  end
end
```

まず、`mount`から確認します。

```elixir:auth_sample/lib/auth_sample_web/live/account_confirmation_live.ex
def mount(%{"token" => token}, _session, socket) do
  form = to_form(%{"token" => token}, as: "account")
  {:ok, assign(socket, form: form), temporary_assigns: [form: nil]}
end
```

mountではurlのパラメータからtokenの値を取得して,その値を`Phoenix.HTML.Form`構造体に変換しています。

`Phoenix.HTML.Form`構造体は以下です。

```elixir
%Phoenix.HTML.Form{
  source: %{"token" => "y-J6QuGXGR9sUlUMqJPYvgRojMxdLL77pvTRWaqdOsw"},
  impl: Phoenix.HTML.FormData.Map,
  id: "account",
  name: "account",
  data: %{},
  hidden: [],
  params: %{"token" => "y-J6QuGXGR9sUlUMqJPYvgRojMxdLL77pvTRWaqdOsw"},
  errors: [],
  options: [],
  index: nil,
  action: nil
}
```

`Phoenix.HTML.Form`構造体をsocketにアサインしてレンダリングしています。

続いて`render`を確認します。

```elixir:auth_sample/lib/auth_sample_web/live/account_confirmation_live.ex
def render(%{live_action: :edit} = assigns) do
  ~H"""
  <div class="mx-auto max-w-sm">
    <.header class="text-center">Confirm Account</.header>

    <.simple_form for={@form} id="confirmation_form" phx-submit="confirm_account">
      <.input field={@form[:token]} type="hidden" />
      <:actions>
        <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
      </:actions>
    </.simple_form>

    <p class="text-center mt-4">
      <.link href={~p"/accounts/register"}>Register</.link>
      |
      <.link href={~p"/accounts/log_in"}>Log in</.link>
    </p>
  </div>
  """
end
```

フォームの送信先は`"confirm_account"`のイベントに送信するみたいです。

では`"confirm_account"`のインベントを確認します。

```elixir:auth_sample/lib/auth_sample_web/live/account_confirmation_live.ex
def handle_event("confirm_account", %{"account" => %{"token" => token}}, socket) do
  case Accounts.confirm_account(token) do
    {:ok, _} ->
      {:noreply,
        socket
        |> put_flash(:info, "Account confirmed successfully.")
        |> redirect(to: ~p"/")}

    :error ->
      case socket.assigns do
        %{current_account: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
          {:noreply, redirect(socket, to: ~p"/")}

        %{} ->
          {:noreply,
            socket
            |> put_flash(:error, "Account confirmation link is invalid or it has expired.")
            |> redirect(to: ~p"/")}
      end
  end
end
```

まず、フォームのパラメータから受け取ったtokenを使用してアカウントを取得します。

```elixir:auth_sample/lib/auth_sample_web/live/account_confirmation_live.ex
Accounts.confirm_account(token)
```

`Accounts.confirm_account/1`関数を確認します。

```elixir:auth_sample/lib/auth_sample/accounts.ex
def confirm_account(token) do
  with  {:ok, query} <- AccountToken.verify_email_token_query(token, "confirm"),
        %Account{} = account <- Repo.one(query),
        {:ok, %{account: account}} <- Repo.transaction(confirm_account_multi(account)) do
    {:ok, account}
  else
    _ -> :error
  end
end

defp confirm_account_multi(account) do
  Ecto.Multi.new()
  |> Ecto.Multi.update(:account, Account.confirm_changeset(account))
  |> Ecto.Multi.delete_all(:tokens, AccountToken.account_and_contexts_query(account, ["confirm"]))
end
```

引数で受け取ったtokenとcontextの`"confirm"`を次の関数に渡しています。

```elixir:auth_sample/lib/auth_sample/accounts.ex
AccountToken.verify_email_token_query(token, "confirm")
```

`AccountToken.verify_email_token_query/2`を確認します。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
@confirm_validity_in_days 7
# ・・・（省略）
def verify_email_token_query(token, context) do
  case Base.url_decode64(token, padding: false) do
    {:ok, decoded_token} ->
      hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
      days = days_for_context(context)

      query =
        from token in token_and_context_query(hashed_token, context),
          join: account in assoc(token, :account),
          where: token.inserted_at > ago(^days, "day") and token.sent_to == account.email,
          select: account

      {:ok, query}

    :error ->
      :error
  end
end

defp days_for_context("confirm"), do: @confirm_validity_in_days

# ・・・（省略）
def token_and_context_query(token, context) do
  from AccountToken, where: [token: ^token, context: ^context]
end
```

まず、引数で受け取ったtokenをデコードします。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
Base.url_decode64(token, padding: false)
```

続いてデコードしたトークンをハッシュ化します。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
```

days変数に整数の7を束縛しています。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
days = days_for_context(context)
```

クエリではトークンが挿入された日が7日以内で、`:sent_to`のメールアドレスとアカウントのメールアドレスが一致していればアカウントを返すものです。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
query =
  from token in token_and_context_query(hashed_token, context),
    join: account in assoc(token, :account),
    where: token.inserted_at > ago(^days, "day") and token.sent_to == account.email,
    select: account
```

最終的にタプルで`{:ok, query}`を返却します。

では`Accounts.confirm_account/1`関数に戻ります。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
%Account{} = account <- Repo.one(query),
```

受け取ったクエリでテーブルからアカウントを取得します。

```elixir:auth_sample/lib/auth_sample/accounts/account_token.ex
{:ok, %{account: account}} <- Repo.transaction(confirm_account_multi(account))

# ・・・（省略）

defp confirm_account_multi(account) do
  Ecto.Multi.new()
  |> Ecto.Multi.update(:account, Account.confirm_changeset(account))
  |> Ecto.Multi.delete_all(:tokens, AccountToken.account_and_contexts_query(account, ["confirm"]))
end
```

トランザクションで`Account`スキーマの`confirmed_at`に現在時刻を割り当てチェンジセットを作成し、更新をします。

```elixir:auth_sample/lib/auth_sample/accounts/account.ex
def confirm_changeset(account) do
  now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
  change(account, confirmed_at: now)
end
```

また、トランザクション内で`context`の値が`"confirm"`でアカウントのidが一致している`AccountToken`のクエリを発行して該当する値をすべて削除します。

```elixir:auth_sample/lib/auth_sample/accounts/account.ex
def account_and_contexts_query(account, [_ | _] = contexts) do
  from t in AccountToken, where: t.account_id == ^account.id and t.context in ^contexts
end
```

では`live`に戻り次を確認します。

```elixir:auth_sample/lib/auth_sample_web/live/account_confirmation_live.ex
def handle_event("confirm_account", %{"account" => %{"token" => token}}, socket) do
  case Accounts.confirm_account(token) do
    {:ok, _} ->
      {:noreply,
        socket
        |> put_flash(:info, "Account confirmed successfully.")
        |> redirect(to: ~p"/")}

    :error ->
      case socket.assigns do
        %{current_account: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
          {:noreply, redirect(socket, to: ~p"/")}

        %{} ->
          {:noreply,
            socket
            |> put_flash(:error, "Account confirmation link is invalid or it has expired.")
            |> redirect(to: ~p"/")}
      end
  end
end
```

`Accounts.confirm_account/1`関数で`confirm_at`に現在時刻を割り当てた状態で更新をし、`"confirm"`の値を持つ`AcccountToken`を削除しました。

正常に処理が終了した場合はhome（`http://localhost:4000/`）にリダイレクトします。
処理が正常に終了しなかった場合は次のどちらかが処理されます。

```elixir:auth_sample/lib/auth_sample_web/live/account_confirmation_live.ex
case socket.assigns do
  %{current_account: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
    {:noreply, redirect(socket, to: ~p"/")}

  %{} ->
    {:noreply,
      socket
      |> put_flash(:error, "Account confirmation link is invalid or it has expired.")
      |> redirect(to: ~p"/")}
end
````

現在のアカウントがすでに2要素認証が済んでいる場合は何もせずにhome（`http://localhost:4000/`）にリダイレクトします。
何らかの理由でアカウントの2要素認証ができなかった場合はhome（`http://localhost:4000/`）にリダイレクトし、警告メッセージを表示させます。

それでは「Confirm my account」ボタンを押して2要素認証をします。

![Screenshot from 2023-03-02 17-48-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646507/3d025d98-9950-0bf8-fa17-ab274915456b.png)


## 終わり

`mix phx.gen.auth`で生成されるコードはPhoenix1.7.0から大幅に変更され、LiveViewでも扱えるようになりました。
LiveViewではセッションに新しく値を登録できないので一度LiveViewを切断してからセッションに値を入れるところに自分も納得したなと感じでした。（今ままでLiveViewでやるにはどうすればいいのかな？と考えていましたがw)

読みづらい文章でしたが、ここまで読んで頂きありがとうございました。

次回は[認証メール再送信](https://qiita.com/suzuki-t0505/items/28bf11f978fcb7a85adb)の部分について見ていきます。