LoginSignup
16
2

More than 1 year has passed since last update.

Phoenix1.7とElixirDesktopでスマホアプリ開発 認証機能編

Last updated at Posted at 2022-12-12

はじめに

本記事は Qiita AdventCalendar2022 Elixir vol4 14日目の記事です

セットアップ編でPhoenixプロジェクトを新たに作成し、それをスマホアプリ化してトップページを表示できるところまでやりました
今回はphx.gen.authを使って認証機能を追加していこうと思います

ElixirDesktopでスマホアプリ作成シリーズ

  1. Phoenix1.7とElixirDesktopでスマホアプリ開発 セットアップ編
  2. Phoenix1.7とElixirDesktopでスマホアプリ開発 認証機能編
  3. Phoenix1.7とElixirDesktopでスマホアプリ開発 CRUD編
  4. Phoenix1.7とElixirDesktopでスマホアプリ開発 GPSと地図アプリ編

認証機能の追加

phx.gen.authを実行します

mix phx.gen.auth Accounts User users

bcrypt_elixirがnifの関係で使えないのでElixir Onlryのpbkdf2_elixirに変更します

mix.exs
defp deps do
    [
-      {:bcrypt_elixir, "~> 3.0"},
+      {:pbkdf2_elixir, "~> 2.0"},
    ]
  end

いつものコマンドを打ちます

mix deps.get
mix ecto.migrate

routing

ログインしていない場合はログインページ
している場合は設定ページに飛ばすようにする

phoenix/lib/spotties_web/controllers/page_controller.ex
defmodule SpottiesWeb.PageController do
  use SpottiesWeb, :controller
  alias Spotties.Accounts.User # 追加

  def home(conn, _params) do
    # The home page is often custom made,
    # so skip the default app layout.
    render(conn, :home, layout: false)
  end

  # 以下追加
  def index(conn, _params) do
    redirect_to(conn, conn.assigns.current_user)
  end

  def redirect_to(conn, %User{}) do
    redirect(conn, to: ~p"/users/settings")
  end

  def redirect_to(conn, nil) do
    redirect(conn, to: ~p"/users/log_in")
  end
end

アプリを開いた時に indexを開くように変更

phoenix/lib/spotties_web/router.ex
defmodule SpottiesWeb.Router do
  ...
  scope "/", SpottiesWeb do
    pipe_through(:browser)
-    get("/", PageController, :home)
+    get("/", PageController, :index)
  end
  ...
end

次の記事で別途ナビゲーションは作成するのでheaderからリンクを削除

phoenix/lib/spotties_web/components/layouts/root.html.heex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title suffix=" · Phoenix Framework">
      <%= assigns[:page_title] || "Spotties" %>
    </.live_title>
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
    </script>
  </head>
  <body class="bg-white antialiased">
-    <ul>
-      <%= if @current_user do %>
-        <li>
-          <%= @current_user.email %>
-        </li>
-        <li>
-          <.link href={~p"/users/settings"}>Settings</.link>
-        </li>
-        <li>
-          <.link href={~p"/users/log_out"} method="delete">Log out</.link>
-        </li>
-      <% else %>
-        <li>
-          <.link href={~p"/users/register"}>Register</.link>
-        </li>
-        <li>
-          <.link href={~p"/users/log_in"}>Log in</.link>
-        </li>
-      <% end %>
-    </ul>
    <%= @inner_content %>
  </body>
</html>

起動すると Sign In のページが開きます
スクリーンショット 2022-12-08 16.16.46.png

sign upのページで実際に登録してみましょう

a99ad0b7816d8c8c328fe5196ec2c911.gif

1.7からphx.gen.authで生成されるページがLiveViewになったので、リアルタイムにバリデーションが行われたりといい感じになっています

SignUpが完了すると / にリダイレクトされるので、上記で設定したとおりにsettingsページに飛ばされます

おまけ:セッション情報の維持

このままでも十分ですが、セッション管理をETS(インメモリDB)で行っている関係上アプリを再起動するとセッション情報が消えるので、アプリを起動する度にログインを要求されます

これはスマホアプリ的に良くないUXなので、セッション情報を維持できるように細工をします

ログイン成功時にファイルにトークンを書き込む

ログイン成功時に実行される関数のmaybe_write_remember_me_cookieを関数パターンマッチでElixirDesktopで実行されている時は別の関数を実行するように細工をします

セットアップ編で指定したconfig_dir配下にトークンを書き込んだファイルを作成します

phoenix/lib/spotties_web/user_auth.ex
defmodule SpottiesWeb.UserAuth do
  ...
  def log_in_user(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    # ↓追加
    params = Map.put(params, "target", Application.fetch_env!(:spotties, :target)) 
    user_return_to = get_session(conn, :user_return_to)

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

  # 以下追加
  defp maybe_write_remember_me_cookie(conn, token, %{"target" => target}) when target != :web do
    File.write!(Spotties.config_dir() <> "/token", Base.encode64(token))
    conn
  end
  ...
end

これでElixirDesktopの時(MIX_TARGETが:host, :android, :ios)の場合にログインが成功したらトークンが各OSのconfigフォルダのファイルに書き込まれます

MIX_TARGETの取得する際にApplication.fetch_env/2を使用しているのは、Config.config_target/0がconfigファイル以外で使うとprodでビルドしているiOSとAndroidではエラーになるので
config.exで環境変数に設定するようにします

phoenix/config/config.exs
import Config
config :spotties, target: config_target()
...

アプリ起動時にトークンを読み込んで、ログインする

トークンを保存するようになったら、起動時に読み込みを行います
アプリを起動して /settingにアクセスしようとすると
以下の流れでログイン中かをチェックされます

phoenix/lib/spotties_web/router.ex
defmodule SpottiesWeb.Router do
  use SpottiesWeb, :router

  # 2 1の1つめの処理browser、レイアウトやセキュリティ周りの処理をまとめて行う
  pipeline :browser do
    ...
    plug(:fetch_current_user) # 3 phx.gen.authで追加された
  end

  ...
  scope "/", SpottiesWeb do
    # 1 /users/settings アクセス前に行う処理がまとめられている
    pipe_through([:browser, :require_authenticated_user]) 

    live_session :require_authenticated_user,
      on_mount: [{SpottiesWeb.UserAuth, :ensure_authenticated}] do
      live("/users/settings", UserSettingsLive, :edit)
      live("/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email)
    end
  end
end
phoenix/lib/spotties_web/user_auth.ex
# 4 connからトークンを取得しユーザーデータを取得する処理
def fetch_current_user(conn, _opts) do
  {user_token, conn} = ensure_user_token(conn)
  user = user_token && Accounts.get_user_by_session_token(user_token)
  assign(conn, :current_user, user)
end

# 5 トークンを取得する処理、セッションにあるか、クッキーにあるか,ないならnilを返す
defp ensure_user_token(conn) do
  if token = get_session(conn, :user_token) do
    {token, conn}
  else
    conn = fetch_cookies(conn, signed: [@remember_me_cookie])
    
    if token = conn.cookies[@remember_me_cookie] do
      {token, put_token_in_session(conn, token)}
    else
      {nil, conn}
    end  
  end
end
  1. pipe_through: ページを開く前に以下の処理を行う
  2. :browser: いろいろやる
  3. :fetch_current_user(:browserの最後): 現在のログイン中のユーザーを取得
  4. UserAuth.fetch_current_user: connからトークンを取得しユーザーデータを取得
  5. UserAuth.ensure_token: connからトークンを取得
    • else側(sessionにトークンなかった場合): cookieから取得
    • cookieにあったらput_token_in_sessionでsessionにトークンを突っ込む

ElixirDesktopではcookieは使わないので、ここの6の処理をMIX_TARGETによって分けて、ファイルからトークンを読み込みましょう

最終的にこうなります
元の処理をrecover_session関数に切り出して、関数パターンマッチで実行する関数を切り替えます
ElixirDesktop側はファイルからトークンを読み込めれば、そのトークンでセッションを新しく貼ります
ない場合は {nil, conn}を返すのはcookieの方とと同じです

phoenix/lib/spotties_web/user_auth.ex
  defp ensure_user_token(conn) do
    if token = get_session(conn, :user_token) do
      {token, conn}
    else
      # recover_sessionに切り出し
      recover_session(conn, Application.fetch_env!(:spotties, :target))
    end
  end

  # 以下追加
  defp recover_session(conn, :web) do
    conn = fetch_cookies(conn, signed: [@remember_me_cookie])

    if token = conn.cookies[@remember_me_cookie] do
      {token, put_token_in_session(conn, token)}
    else
      {nil, conn}
    end
  end

  defp recover_session(conn, _) do
    case File.read(Spotties.config_dir() <> "/token") do
      {:ok, token} ->
        {token, put_token_in_session(conn, Base.decode64!(token))}

      {:error, :enoent} ->
        {nil, conn}
    end
  end
  ...
end

これで起動時にセッションを復活させることができました

ログアウト時にトークンを削除する

最後にログアウトした際にトークンファイルを削除し、ログアウトしたはずなのにログインした状態になることを防ぎます

phoenix/lib/spotties_web/user_auth.ex
  def log_out_user(conn) do
    user_token = get_session(conn, :user_token)
    user_token && Accounts.delete_user_session_token(user_token)
    File.rm(Spotties.config_dir() <> "/token") # 追加

    if live_socket_id = get_session(conn, :live_socket_id) do
      SpottiesWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
    end

    conn
    |> renew_session()
    |> delete_resp_cookie(@remember_me_cookie)
    |> redirect(to: "/")
  end
  ...
end

以上で完了になります

動作確認

eac75578f5402d917dea66dfa9aef753.gif

無事セッションの維持、破棄ができるようになりました

最後に

ElixirDesktopのアプリにphx.gen.authで認証機能を追加できました
次はナビゲーション周りとphx.gen.liveで CRUD機能を追加していこうと思います
本記事は以上になりますありがとうございました

16
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
2