LiveViewで認証付きのチャットアプリを構築する(認証機能作成)

はじめに

前回では、プロジェクトの作成からユーザー登録機能の作成までを行いました。
引き続き認証まわりを実装していきます。
本記事で、認証機能が完成します。

本テーマについて

本テーマは7記事構成になっています。
7記事それぞれで取り扱う内容は以下です。

  1. プロジェクト作成からログイン処理まで
  2. 認証機能の作成(本記事)
  3. チャットボードのコンテキスト周り作成
  4. チャットボードの画面周り作成
  5. Phoenix.PubSubを使用したリアルタイム更新
  6. Phoenix.Presenceを使用したアクティブユーザー表示
  7. 入力ステータスの表示

本テーマは以下の方針で記述します。

  • 機能実装は本テーマに沿った必要最小限のみ
  • UIもほぼそのまま
  • 機能へのアクセスはURLで直に指定を想定

完成版の画面イメージは以下のようになっています。

lv_chat

認証機能の作成

アクセスしてきたユーザーに対し、認証および認可を行う機能を作成してゆきます。

ログインページの作成

以下の手順でログインページを作っていきます。

  1. 認証用関数の追加
  2. Routerへセッション操作用APIの追加
  3. SessionControllerの作成
  4. Viewの作成
  5. Templateの作成

Accountsモジュールへ認証用関数の追加

認証用関数を追加します。
ユーザー検証が成功した場合には、DBから取得したユーザーデータを返します。

# /lib/lv_chat/accounts.ex
def get_user_by(params) do
    Repo.get_by(User, params)
end

def get_user_by_name(name) do
    get_user_by(name: name)
end

def authenticate_by_name_and_pass(name, given_pass) do  user = get_user_by_name(name)

  cond do
  user && Pbkdf2.verify_pass(given_pass, user.password_hash) ->
    {:ok, user}
  user ->
    {:error, :unauthorized}
  true ->
    Pbkdf2.no_user_verify()
    {:error, :not_found}
  end
end

Routerへセッション操作用APIの追加

Routerへセッション操作用のAPIを追加します。

# /lib/lv_chat_web/router.ex
scope "/", RumblWeb do
  pipe_through :browser
  ...
  resources "/sessions", SessionController, only: [:new, :create, :delete]
  ...
end

追加されるのは以下になります。

関数urlmethodタイミング
new/sessions/newGETログインページの取得時
create/sessions/POSTログインボタン押下時
delete/sessions/:idDELETEログアウト選択時

SessionControllerの作成

/lib/lv_chat_web/controllers/session_controller.ex というファイルを作成し、以下のように実装します。
認証が通ったときのログイン処理では、セッション変数にユーザーIDを格納します。
このセッション情報があれば、認証済みとして扱われます。
ログイン処理については、前回の記事の以下で記述しています。
ログイン処理

# /lib/lv_chat_web/controllers/session_controller.ex
defmodule LvChatWeb.SessionController do
  use LvChatWeb, :controller

  def new(conn, _) do
    render(conn, "new.html")
  end

  def create(conn, %{"session" => %{"name" => name, "password" => pass}}) do
    case LvChat.Accounts.authenticate_by_name_and_pass(name, pass) do      {:ok, user} ->
        conn
        |> LvChatWeb.Auth.login(user)
        |> put_flash(:info, "Welcome back!")
        |> redirect(to: Routes.page_path(conn, :index))
      {:error, _reason} ->
        conn
        |> put_flash(:error, "無効なユーザー名とパスワードの組み合わせです。")
        |> render("new.html")
    end
  end

  def delete(conn, _) do
    conn
    |> LvChatWeb.Auth.logout()
    |> redirect(to: Routes.page_path(conn, :index))
  end
end

Viewの作成

/lib/lv_chat_web/views/session_view.ex というファイルを作成し、以下のように実装します。

# /lib/lv_chat_web/views/session_view.ex
defmodule LvChatWeb.SessionView do
  use LvChatWeb, :view
end

Templateの作成

/lib/lv_chat_web/templates/session というディレクトリを作成し、 そこに new.html.eex というファイルを作成します。

# /lib/lv_chat_web/templates/session/new.html.eex
<h1>Login</h1>

<%= form_for @conn, Routes.session_path(@conn, :create),[as: :session], fn f -> %>
  <div>
    <%= text_input f, :name, placeholder: "Username" %>
  </div>
  <div>
    <%= password_input f, :password, placeholder: "Password" %>
  </div>
  <%= submit "Log in" %>
<% end %>

認可機能の作成

セッション処理用Plugの作成

ユーザー情報を :current_user にアサインします。
適切なユーザー情報がアサインできない場合は、 nil を指定します。
この処理は認証が必要なすべてのページで必要となるので、Plug として作成します。

モジュールPlugの実装

セッション処理を行う Plug を作成します。
モジュールPlugとして作成します。

そのためには、init/1call/2 を実装します。

# /lib/lv_chat_web/controllers/auth.ex
defmodule LvChatWeb.Auth do
  import Plug.Conn  import Phoenix.Controller

  alias LvChatWeb.Router.Helpers, as: Routes

  def init(opts), do: opts
  def call(conn, _opts) do    user_id = get_session(conn, :user_id)
    cond do
      user = conn.assigns[:current_user] ->
        put_current_user(conn, user)
      user = user_id && LvChat.Accounts.get_user!(user_id) ->
        put_current_user(conn, user)
      true ->
        assign(conn, :current_user, nil)
    end
  end
  ...
end

作成したPlugをパイプラインへ

作成したPlugをRouterのパイプラインに仕込みます。

# /lib/lv_chat_web/router.ex
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_flash
  plug :protect_from_forgery
  plug :put_secure_browser_headers
  plug LvChatWeb.Authend

これでbrowserパイプラインを通るアクセス全てに、作成したPlugの処理が通るようになります。

認可処理

制限ページへのアクセスに対する振り分けを行います。
パイプラインで LvChatWeb.Auth を通ってきたことが前提となります。
さきほど作成した Plug により、ユーザー情報が読み込まれている場合にはそのまま通過させます。
読み込まれていない場合にはログインページへリダイレクトします。

current_user処理
値が存在そのまま通過
nilログインページへ
# /lib/lv_chat_web/controllers/auth.ex
def authenticate_user(conn, _opts) do
  if conn.assigns.current_user do
    conn
  else
    conn
    |> put_flash(:error, "本ページにアクセスするにはログインが必要です。")
    |> redirect(to: Routes.session_path(conn, :new))
    |> halt()
  end
end

認証関数のデフォルトでのインポート

認証処理用関数をcontrollerやrouterで使用できるようにこの関数をインポートしておきます。

# /lib/lv_chat_web.ex
def controller do
  quote do
    use Phoenix.Controller, namespace: LvChatWeb

    import Plug.Conn
    import LvChatWeb.Gettext
    import LvChatWeb.Auth, only: [authenticate_user: 2]    alias LvChatWeb.Router.Helpers, as: Routes
  end
end
# lib/lv_chat_web.ex
def router do
  quote do
    use Phoenix.Router
    import Plug.Conn
    import Phoenix.Controller
    import LvChatWeb.Auth, only: [authenticate_user: 2]  end
end

NavBarへセッション処理用メニューの追加

現状のNavBarについて、以下の情報が表示されるように変更します。

ステータス表示要素
非ログイン時
  • 新規登録
  • ログイン
ログイン時
  • ユーザー名
  • ログアウト

具体的には、現状の以下の部分を

# /lib/lv_chat_web/templates/layout/app.html.eex
<nav role="navigation">
  <ul>
    <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
  </ul>
</nav>

以下のように変更します。

# /lib/lv_chat_web/templates/layout/app.html.eex
<nav role="navigation">
  <ul>
    <%= if @current_user do %>      <li><%= @current_user.name %></li>      <li>        <%= link "Log out",              to: Routes.session_path(@conn, :delete, @current_user),              method: "delete" %>      </li>    <% else %>      <li><%= link "Register", to: Routes.user_path(@conn, :new) %></li>      <li><%= link "Log in", to: Routes.session_path(@conn, :new) %></li>    <% end %>  </ul>
</nav>

これで認証部分は完了です。

要認証ページの定義

セッション処理を行うプラグインを作成します。

プラグイン

今まで作成したページの中に、認証が必要なものとそうでないものがありました。
それを分け、認証が必要なページには通常のパイプラインのあとに :authenticate_user/2 を呼び出すようにします。

今まで作成したページの中では以下のようになります。

ログインが必要?ページ
いいえ
  • ユーザーの新規追加
  • ログイン
はい
  • コンテンツ全体
  • ログアウト

これらをもとに以下のように実装します。

# /lib/lv_chat_web/router.ex
scope "/", LvChatWeb do
  pipe_through :browser

  resources "/users", UserController, only: [:new, :create]
  resources "/sessions", SessionController, only: [:new, :create]
end

scope "/", LvChatWeb do
  pipe_through [:browser, :authenticate_user]
  get "/", PageController, :index
  resources "/sessions", SessionController, only: [:delete]
end

上記において、1つ目の scope ブロックが認証を必要としないものであり、2つ目が認証を必要とするものです。

終わりに

本記事で、認証機能が完成しました。
いよいよ次回は、チャット機能に関係するところに入っていきます。
次回は以下になります。

チャットボードのコンテキスト周り作成