はじめに
この記事は Elixirアドベントカレンダーのシリーズ4の18日目の記事です
今回はAPIレス開発での認証機能とセッションの保持について解説します
phx.gen.authでひな形作成
以下のコマンドでデフォルトの認証機能を生成します
mix phx.gen.auth Accounts User users
ライブラリの差し替え
上記のコマンドで bcrypt_elixirが追加されていますが、こちらはELixirDesktopでは使えないので別のライブラリに差し替えます
defp deps do
[
- {:bcrypt_elixir, "~> 3.0"},
+ {:pbkdf2_elixir, "~> 2.0"}
{:phoenix, "~> 1.7.10"},
変更したら以下を実行します
mix deps.get
mix ecto.migrate
コード内のBcryptを使用している箇所も変更していきます
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にもあるので忘れないようにしましょう
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で統一する必要がありますので気をつけましょう
ログイン有無でのリダイレクト処理
アプリを起動した時に、ログインしていたら設定ページ、していなかったらログインページにリダイレクトするようにします
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
トップページで開くパスを変更します
scope "/", SpotterWeb do
pipe_through :browser
- get "/", PageController, :home
- get "/", PageController, :index
end
セッション保持
ElixirDesktopはセッション情報をetsというインメモリDBで管理するのでアプリを終了するとセッション情報は消えてしまうので起動毎にログインを要求されます
これはUX的に良くないのでセッション情報を保持するようにします
maybe_write_remember_me_cookie
関数を書き換えてログイン時にトークンをテキストファイルに書き出します
- 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にトークンがなかった場合はクッキーを見に行っているのをトークンファイルを見に行くように変更します
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
これで起動時にセッションを復活できるようになりました
ログアウト時にトークンを削除する
ログアウトをしてもトークンファイルが残り続けて、起動の度にログインされるので、ログアウト時にトークンファイルを削除するようにします
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
動作確認
セッション保持とログアウトで破棄されているのを確認できました
最後に
本記事ではAPIレスアプリ開発で認証機能とセッション保持を実装しました
端末にデータを保存するときは config_dirの配下に置けば良いので楽ですね
平文で保存しておくのが抵抗がある場合はdetsを使えば良さそうですね
次はナビゲーションとCRUDを実装してきます
本記事は以上になりますありがとうございました