エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Pow で認証したユーザを LiveView で参照するときの覚書

先日書いた phx_gen_auth とおなじく認証のしくみを Phoenix に組みこむためパッケージ。

phx_gen_auth が認証のしくみを実現するコードを生成するライブラリなのに対し(なので phx_gen_auth 自体はアプリケーション内で利用されない)、Pow はアプリケーションの一部として実行時に認証のしくみを提供するのが大きな違い。

hex.pm

結論

独自の authorization plug を実装し、トークンをセッションに登録する。 LiveView でそのトークンを受け取りユーザを取得する。

実例

アプリケーションを用意する

$ mix phx.new my_app --live
$ cd my_app

Pow を追加する

mix.exsdepspow を追加、パッケージを取得しコンパイルする。

defmodule MyApp.MixProject do
  use Mix.Project

  # ...

  def deps do
    # ...
    {:pow, "~> 1.0"}
  end
end
$ mix do deps.get, deps.compile

認証のしくみを用意する

mix pow.install タスクを利用して、認証のしくみに必要なファイルを生成する。 あわせて既存のファイルに対する変更が表示されるので、それにしたがっってファイルを編集する。

なお、Endpoint には後述するようにカスタマイズした独自のモジュールを指定するので、 Endpoint の編集はそこで説明する。

$ mix pow.install
* creating priv/repo/migrations
* creating priv/repo/migrations/20200915121951_create_users.exs
* creating lib/my_app/users
* creating lib/my_app/users/user.ex
Pow has been installed in your phoenix app!

There are three files you'll need to configure first before you can use Pow.

First, append this to `config/config.exs`:

config :my_app, :pow,
  user: MyApp.Users.User,
  repo: MyApp.Repo

Next, add `Pow.Plug.Session` plug to `lib/my_app_web/endpoint.ex` after `plug Plug.Session`:

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # ...

  plug Plug.Session, @session_options
  plug Pow.Plug.Session, otp_app: :my_app
  plug MyAppWeb.Router
end

Last, update `lib/my_app_web/router.ex` with the Pow routes:

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Pow.Phoenix.Router

  # ... pipelines

  scope "/" do
    pipe_through :browser

    pow_routes()
  end

  # ... routes
end

Config

config/config.exs に Pow の設定を追加する。

config :my_app, :pow,
  user: MyApp.Users.User,
  repo: MyApp.Repo

Router

lib/my_app_web/router.ex で定義されるモジュール MyAppWeb.RouterPow.Phoenix.Router を use する。

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Pow.Phoenix.Router # 追加

  # ...
end

scope を追加し pow_routes/0 を追加する。 これによって認証に必要な登録やログインのルーティングが追加される。

defmodule MyAppWeb.Router do
  # ...

  scope "/" do
    pipe_through :browser

    pow_routes()
  end

  # ...
end

pipeline :protected を追加する。

defmodule MyAppWeb.Router do
  # ...

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated,
      error_handler: Pow.Phoenix.PlugErrorHandler
  end

  # ...
end

LiveView を含む scope の pipe_through:protected を追加する。 これによって LiveView のリソースにアクセスするにはログインが必要になる。

defmodule MyAppWeb.Router do
  # ...

  scope "/", MyAppWeb do
    pipe_through [:browser, :protected] # :protected を追加


    live "/", PageLive, :index
    live "/user", UserLive # 追加
  end

  # ...
end

Authorization Plug

ドキュメントにある独自の authorization plug を定義する。

ファイルはどこに配置してもよいけれども、ここではモジュール名に合わせて lib/my_app_web/pow/plug.ex に配置する。

ログインのときに呼び出される create/3 で、セッションにユーザ ID にもとづくトークンを記録する。 LiveView ではこのトークンを利用してログインしているユーザを取得する。

defmodule MyAppWeb.Pow.Plug do
  use Pow.Plug.Base

  @session_key :pow_user_token
  @salt "user salt"
  @max_age 86400

  @impl true
  def fetch(conn, _config) do
    conn  = Plug.Conn.fetch_session(conn)
    token = Plug.Conn.get_session(conn, @session_key)

    MyAppWeb.Endpoint
    |> Phoenix.Token.verify(@salt, token, max_age: @max_age)
    |> maybe_load_user(conn)
  end

  defp maybe_load_user({:ok, user_id}, conn), do: {conn, MyApp.Repo.get(MyApp.Users.User, user_id)}
  defp maybe_load_user({:error, _any}, conn), do: {conn, nil}

  @impl true
  def create(conn, user, _config) do
    token = Phoenix.Token.sign(MyAppWeb.Endpoint, @salt, user.id)
    conn  =
      conn
      |> Plug.Conn.fetch_session()
      |> Plug.Conn.put_session(@session_key, token)

    {conn, user}
  end

  @impl true
  def delete(conn, _config) do
    conn
    |> Plug.Conn.fetch_session()
    |> Plug.Conn.delete_session(@session_key)
  end
end

Endpoint

lib/my_app_web/endpoint.ex に上で定義した plug を設定する。

Pow.PlugSession の結果に依存し、RouterPow.Plug の結果に依存するため、この位置に記述する。

defmodule MyAppWeb.Endpoint do
  # ...

  plug Plug.Session, @session_options
  plug MyAppWeb.Pow.Plug, otp_app: :my_app # Session の後、Router の前のこの位置に追加
  plug MyAppWeb.Router
end

LiveView

認証を必要とする LiveView を定義する。

mount/3 の第二引数のセッション情報から、先に記述した plug の create/3 で記録したトークンを取得する。 このトークンからユーザ ID を復元し、ユーザのデータを取得する。

defmodule MyAppWeb.UserLive do
  use MyAppWeb, :live_view

  @salt "user salt"
  @max_age 86400

  def mount(_params, %{"pow_user_token" => token}, socket) do
    {:ok, user_id} = Phoenix.Token.verify(MyAppWeb.Endpoint, @salt, token, max_age: @max_age)
    current_user = MyApp.Repo.get(MyApp.Users.User, user_id)

    {:ok, assign(socket, current_user: current_user)}
  end

  def render(assigns) do
    ~L"""
    <%= @current_user.email %>
    """
  end
end

サーバを起動し動作を確認する

サーバを起動し、追加した LiveView の URL http://localhost:4000/user にアクセスする。

$ iex -S mix phx.server 

認証されていないのでログインページにリダイレクトされる。

ユーザ登録のためにログインページに表示されているリンク Register をクリックする。

表示されるユーザ登録ページでメールアドレスとパスワードと確認用のパスワードを入力する。

ユーザ登録が完了すると、LiveView のページが表示され、登録したメールアドレスが表示される。

いつか読むはずっと読まない: Peytoia nathorsti

石化した古生物を扱う学問であっても、それもまたダイナミックな人間の営みであることをいつも感じます。

アノマロカリス解体新書

アノマロカリス解体新書

  • 作者:土屋 健
  • 発売日: 2020/02/12
  • メディア: 単行本(ソフトカバー)