11
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

はじめに

この記事は Elixirアドベントカレンダーのシリーズ4の18日目の記事です

今回はAPIレス開発での認証機能とセッションの保持について解説します

phx.gen.authでひな形作成

以下のコマンドでデフォルトの認証機能を生成します

mix phx.gen.auth Accounts User users

ライブラリの差し替え

上記のコマンドで bcrypt_elixirが追加されていますが、こちらはELixirDesktopでは使えないので別のライブラリに差し替えます

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

変更したら以下を実行します

mix deps.get
mix ecto.migrate

コード内のBcryptを使用している箇所も変更していきます

lib/spotter/accounts/user.ex
defmodule Spotter.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  ...
  # L62
  defp maybe_hash_password(changeset, opts) do
    hash_password? = Keyword.get(opts, :hash_password, true)
    password = get_change(changeset, :password)

    if hash_password? && password && changeset.valid? do
      changeset
-     # If using Bcrypt, then further validate it is at most 72 bytes long
+     # If using Pbkdf2, then further validate it is at most 72 bytes long
      |> validate_length(:password, max: 72, count: :bytes)
      # Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that
      # would keep the database transaction open longer and hurt performance.
-     |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
+     |> put_change(:hashed_password, Pbkdf2.hash_pwd_salt(password))
      |> delete_change(:password)
    else
      changeset
    end
  end

  ...
  # L131
  @doc """
  Verifies the password.

  If there is no user or the user doesn't have a password, we call
-  `Bcrypt.no_user_verify/0` to avoid timing attacks.
+  `Pbkdf2.no_user_verify/0` to avoid timing attacks.
  """
  def valid_password?(%Spotter.Accounts.User{hashed_password: hashed_password}, password)
      when is_binary(hashed_password) and byte_size(password) > 0 do
-   Bcrypt.verify_pass(password, hashed_password)
+   Pbkdf2.verify_pass(password, hashed_password)
  end

  def valid_password?(_, _) do
-   Bcrypt.no_user_verify()
+   Pbkdf2.no_user_verify()    
    false
  end
  ...
end

test.exsにもあるので忘れないようにしましょう

config/test.exs
import Config

# Only in tests, remove the complexity from the password hashing algorithm
- config :bcrypt_elixir, :log_rounds, 1
+ config :pbkdf2_elixir, :log_rounds, 1

Webアプリも作る場合は、暗号化アルゴリズムが同じである必要があるため両方pbkdf2で統一する必要がありますので気をつけましょう

ログイン有無でのリダイレクト処理

アプリを起動した時に、ログインしていたら設定ページ、していなかったらログインページにリダイレクトするようにします

lib/spotter_web/controllers/page_controller.ex
defmodule SpotterWeb.PageController do
  use SpotterWeb, :controller
+ alias Spotter.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

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

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

トップページで開くパスを変更します

lib/spotter_web/router.ex
  scope "/", SpotterWeb do
    pipe_through :browser

-   get "/", PageController, :home
-   get "/", PageController, :index
  end

セッション保持

ElixirDesktopはセッション情報をetsというインメモリDBで管理するのでアプリを終了するとセッション情報は消えてしまうので起動毎にログインを要求されます

これはUX的に良くないのでセッション情報を保持するようにします

maybe_write_remember_me_cookie関数を書き換えてログイン時にトークンをテキストファイルに書き出します

lib/spotter_web/user_auth.ex:L39
- defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
-   put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
+  defp maybe_write_remember_me_cookie(conn, token, _params) do
+   File.write!(Spotter.config_dir() <> "/token", Base.encode64(token))
+   conn    
  end

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

セッション復活

起動時にセッションを復活できるようにします
以下の関数がfetch_current_userで実行されているので、sessionにトークンがなかった場合はクッキーを見に行っているのをトークンファイルを見に行くように変更します

lib/spotter_web/user_auth.ex:L98
  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}
+     case File.read(Spotter.config_dir() <> "/token") do
+       {:ok, token} ->
+         {token, put_token_in_session(conn, Base.decode64!(token))}
+ 
+       {:error, :enoent} ->
+         {nil, conn}
      end      
    end
  end

これで起動時にセッションを復活できるようになりました

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

ログアウトをしてもトークンファイルが残り続けて、起動の度にログインされるので、ログアウト時にトークンファイルを削除するようにします

lib/spotter_web/user_auth.ex:L74
  def log_out_user(conn) do
    user_token = get_session(conn, :user_token)
    user_token && Accounts.delete_user_session_token(user_token)
+   File.rm(Spotter.config_dir() <> "/token")

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

    conn
    |> renew_session()
-   |> delete_resp_cookie(@remember_me_cookie)
    |> redirect(to: ~p"/")
  end

動作確認

セッション保持とログアウトで破棄されているのを確認できました

b91ae56de77a6d353c399826ef6c14be.gif

最後に

本記事ではAPIレスアプリ開発で認証機能とセッション保持を実装しました
端末にデータを保存するときは config_dirの配下に置けば良いので楽ですね
平文で保存しておくのが抵抗がある場合はdetsを使えば良さそうですね

次はナビゲーションとCRUDを実装してきます

本記事は以上になりますありがとうございました

11
1
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
11
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?