LoginSignup
3
0

More than 1 year has passed since last update.

Phoenixで作るGPS Logging System 6 APIの認証2

Last updated at Posted at 2021-12-08

はじめに

ひとりLiveView Advent Calendar の6日目の記事です

この記事はElixir Conf US 2021発表したシステムの構築と関連技術の解説を目的とした記事です

キーボードで入力が困難なデバイスまたはemail,passwordを保存したくないデバイス(Apple Watch, raspberry pi等)で認証トークン発行するために再発行が容易なパスコードを使用できるようにします。

今回は以下の3つを実装します

  • Userにpasscodeカラム追加
  • passcode発行画面
  • passcode認証

Userにpasscodeカラム追加

カラムを追加する場合は以下のコマンドで空のmigrationファイルを作成します

mix ecto.gen.migration add_passcode_to_users

カラムを追加する場合はalter table(table名) doで囲って追加するカラムを記述します

priv/repo/migrations/20211208151908_add_passcode_to_users.exs
defmodule LiveLogger.Repo.Migrations.AddPasscodeToUsers do
  use Ecto.Migration

  def change do
    alter table("users") do
      add :passcode, :string
    end
  end
end

DBに反映させます

mix ecto.migrate

field追加

lib/live_logger/accounts/user.ex
defmodule LiveLogger.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string

    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :naive_datetime
    field :passcode, :string # 追加

    timestamps()
  end
  ...
end

これでpasscodeカラムが追加できました

passcode発行画面

次にパスコード発行ボタンをusers/settingsに追加します

passcodeを生成するだけのchangesetを作成します
15桁の数値を保存しようとすると桁数が多すぎると怒られるため、string型にしています

lib/live_logger/accounts/user.ex
defmodule LiveLogger.Accounts.User do
  ...
  def passcode_changeset(user) do
    user
    |> cast(%{}, [:passcode])
    |> put_change(
      :passcode,
      Enum.random(100_000_000_000_000..999_999_999_999_999)
      |> to_string()
    )
  end
  ...
end

Accountsでpasscodeをupdateする関数を追加

lib/live_logger/accounts.ex
defmodule LiveLogger.Accounts do
  ...
  def generate_passcode(%User{} = user) do
    user
    |> User.passcode_changeset()
    |> Repo.update()
  end
  ...
end

衝突しないようにpasscodeをuniqueにすればエラーが出るかもしれませんが、今回はしないので正常系のみ実装

lib/live_logger_web/controllers/user_settings_controller.ex
defmodule LiveLoggerWeb.UserSettingsController do
  ... 
  def generate_passcode(conn, _params) do
    case Accounts.generate_passcode(conn.assigns.current_user) do
      {:ok, _ } -> 
        conn
        |> put_flash(:info, "Passcode generate successfully.")
        |> redirect(to: Routes.user_settings_path(conn, :edit))
    end
  end
  ...
end

テンプレートがheexになってform_forが使えないので、linkをmethod postにしてformの代替にしています

lib/live_logger_web/templates/user_settings/edit.html.heex
<h1>Settings</h1>

<h3>Generate Passcode</h3>
<%= link "Generate Passcode", 
          to: Routes.user_settings_path(@conn, :generate_passcode), 
          method: :post, 
          class: "button is-info" 
%>
<%= if @conn.assigns.current_user.passcode do %>
  <p><%= "passcode:" <> @conn.assigns.current_user.passcode %></p>
<% end %>
...

routerに追加

lib/live_logger_web/router.ex
defmodule LiveLoggerWeb.Router do
  ...
  scope "/", LiveLoggerWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings", UserSettingsController, :edit
    put "/users/settings", UserSettingsController, :update
    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
    post "/users/settings/generate_passcode", UserSettingsController, :generate_passcode # 追加

    live "/maps", MapLive.Index, :index
    live "/maps/new", MapLive.Index, :new
    live "/maps/:id/edit", MapLive.Index, :edit

    live "/maps/:id", MapLive.Show, :show
    live "/maps/:id/show/edit", MapLive.Show, :edit
  end
  ...
end

動作確認をしていきます

スクリーンショット 2021-12-09 1.53.42.png
ボタンを押してpasscodeが発行されるのを確認できました

スクリーンショット 2021-12-09 1.58.38.png

passcode認証

最後に認証部分を実装します

passcodeでuserを取得するクエリーを追加します

lib/live_logger/accounts.ex
defmodule LiveLogger.Accounts do
  ...
  def get_user_by_passcode(passcode) when is_binary(passcode) do
    Repo.get_by(User, passcode: passcode)
  end
  ...
end

paramsの部分の引数のパターンマッチを変えているので、同名の関数でも処理を分けて実行することができます

lib/live_logger_web/controllers/user_api_session_controller.ex
defmodule LiveLoggerWeb.UserApiSessionController do
  ...
  def create(conn, %{"email" => email, "password" => password}) do
    with user when is_struct(user) <- Accounts.get_user_by_email_and_password(email, password),
         token <- user |> Accounts.generate_user_session_token() |> Base.encode64() do
      render(conn, "token.json", token: token)
    else
      _error -> {:error, :unauthorized}
    end
  end

  # 以下追加
  def create(conn, %{"passcode" => passcode}) do
    with user when is_struct(user) <- Accounts.get_user_by_passcode(passcode),
         token <- user |> Accounts.generate_user_session_token() |> Base.encode64() do
      render(conn, "token.json", token: token)
    else
      _error -> {:error, :unauthorized}
    end
  end
  ...
end

動作確認をしていきます

スクリーンショット 2021-12-09 2.15.03.png
先程発行したpasscodeで認証トークンを取得することができました

最後に

これでemail, passwordで認証トークンが発行できないデバイスをパスコードを使用して認証トークンを発行できるようになりました
本記事は以上になります

次はLiveViewでGoogleMapを表示する部分を実装します

code

3
0
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
3
0