LoginSignup
15
5

Brightコードハック②Elixirプロダクション攻略:【認証】独自ID+SNSログインをphx_gen_auth+Überauthでサクッと作る

Last updated at Posted at 2023-12-16

この記事は、Elixir Advent Calendar 2023 シリーズ14 の16日目です


piacere です、ご覧いただいてありがとございます :bow:

今年後半にαリリースした、過去/現在/未来のスキルから、あなたのBright(輝き)とRight(正しさ)を引き出すプロダクト「Bright」の実装から、Elixirプロダクション開発のヒントをお届けするシリーズです

前回は、Elixir Web CRUDの基本として、LiveView Scaffold+Tailwindで「スキルパネル」そっくりさん(下記赤枠)を爆速開発 する内容でした
image.png

今回は、Phoenix標準の独自ID認証機能「phx_gen_auth」と、SNS認証(Google/GitHub/Twitterなどのアカウントでの認証)を叶えるライブラリ「Überauth」で、Bright「ログイン」そっくりさんを作ってみましょう
image.png

なお、Brightが生まれた背景は、昨年Elixir Advent Calendarで下記にまとめています(400いいねを超える人気コラム)

あと、このコラムが、面白かったり、役に立ったら、image.png をお願いします :bow:

「phx_gen_auth」による独自ID認証はコマンド1発

phx_gen_authを使うと、認証処理用テーブルとユーザー登録/ログイン画面のScaffold自動生成ができます

なお、実際のBrightにおける各テーブルIDは、UUIDを利用していますが、ここでは簡便化のためにintegerで定義します

今回は、前回作ったPhoenix PJに機能追加していく形で進行しますので、作成が未だであれば、前回コラムをお試しいただいて から、本コラムに戻って下記を実施してください

認証をScaffold自動生成

早速、Scaffoldしてみましょう(Phoenix起動まで一気に行います)

mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.migrate
iex -S mix phx.server

routerは勝手に追加されるので、手動追加不要です(エンドポイントを変更したり、一部塞ぎたい場合は手動で行ってください)

ブラウザで http://localhost:4000/ にアクセスすると、右上に「Register」「Log in」リンクが追加されるので、「Register」でユーザー登録してみましょう
image.png

入力し、ボタン押下します
image.png

ユーザー登録が行われ、ログイン済み状態に右上リンクが変わるので、「Log out」し、「Log in」リンクを押下してみましょう
image.png

先ほど作成したユーザーでログインできることが確認できると思います
image.png

このようにphx_gen_authは、独自ID認証をとてもカンタンに実現してくれる強力なScaffoldです

ユーザー登録時にメール確認を挟む

上記で、ユーザー登録時にメールで確認を行いませんでしたが、実は確認のためのURLが載ったメールは投げられており、ブラウザで http://localhost:4000/dev/mailbox をご覧ください
image.png

このメールを実際に投げれるようにしたり、メール本文をカスタマイズしたい場合は、下記コラムのSwoosh部分をご参考ください

あとは、ユーザー登録と同時にログインさせないようにするだけですが、こちらは下記2箇所のコード改修だけで実現できます

lib/bright/accounts/user.ex
defmodule Bright.Accounts.User do

- def valid_password?(%Bright.Accounts.User{hashed_password: hashed_password}, password)
+ def valid_password?(%Bright.Accounts.User{hashed_password: hashed_password, confirmed_at: confirmed_at}, password)
-     when is_binary(hashed_password) and byte_size(password) > 0 do
+     when is_binary(hashed_password) and byte_size(password) > 0 and is_nil(confirmed_at) == false do
    Bcrypt.verify_pass(password, hashed_password)
  end

lib/bright_web/controllers/user_session_controller.ex
defmodule BrightWeb.UserSessionController do

  def create(conn, %{"_action" => "registered"} = params) do
-   create(conn, params, "Account created successfully!")
+   conn
+   |> put_flash(:info, "Please log in after accessing the registration confirmation URL in the e-mail.")
+   |> redirect(to: ~p"/users/log_in")
  end

1つ目のコードで、users テーブルの confirmed_at が入っていないときはログイン不可にでき、メールで送られたURLを踏むと、confirmed_at に日時が入って、ログイン可能となる仕組みです
image.png

2つ目のコードで、ユーザー登録直後をアカウント作成完了とせず、ログイン画面で登録確認メールのチェックを促していますが、もしこのコードを入れないと、下記 if 式で confirmed_at が入っておらずfalseで else の「Invalid email or password」表示が行われ、ユーザーが混乱するので対応をお忘れなく

lib/bright_web/controllers/user_session_controller.ex
defmodule BrightWeb.UserSessionController do

  defp create(conn, %{"user" => user_params}, info) do
    %{"email" => email, "password" => password} = user_params

    if user = Accounts.get_user_by_email_and_password(email, password) do
    
    else
      # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
      conn
      |> put_flash(:error, "Invalid email or password")
      |> put_flash(:email, String.slice(email, 0, 160))
      |> redirect(to: ~p"/users/log_in")
    end

メールのURLにアクセスすると、下記画面になり、ボタン押下後にログインが可能となるので、試してみてください
image.png

オマケ:バリデーションのカスタマイズ

デフォルトのphx_gen_authは、パスワード最低長が12文字と長いため、カスタマイズするときは下記コラムを参考にしてください

「Überauth」によるSNS認証

Überauthを使うと、さまざまなSNSアカウントでの認証をカンタンに実装できるようになります

今回は、そのGoogleアカウント向けライブラリである下記を使います

事前準備:ログイン画面に「Googleログイン」ボタン追加

その前に、phx_gen_authのログイン画面に、「Googleログイン」ボタンを追加しておきましょう

phx_gen_authがどのコンポーネントで定義されているかは、LiveView 0.20で使えるようになった下記の技を使ってブラウザ上から特定してみましょう

ログイン画面が、下記コード/行数で定義されていることが、いとも容易く特定できました

image.png

試しに、「Googleログイン」ボタンを追加してみましょう

lib/bright_web/live/user_login_live.ex
defmodule BrightWeb.UserLoginLive do

<:actions>
          <.button phx-disable-with="Signing in..." class="w-full">
            Sign in <span aria-hidden="true"></span>
          </.button>
        </:actions>
      </.simple_form>
+     <p class="py-4">
+       <.link href={~p"/auth/google"} class="w-full ">
+         <.button class="w-full bg-gray-100 bg-[url('https://app.bright-fun.org/images/bg_google.png')] bg-left bg-no-repeat border border-solid border-black hover:opacity-40">
+           <span class="text-black">Googleログイン</span>
+         </.button>
+       </.link>
+     </p>

ボタン自体はテキトーなのでご愛嬌 :smile:
image.png

これまでは、画面文言などから推定していたのが、LiveViewフロントデバッグのおかげで嘘のようにカンタンです

Überauthの導入

Überauth Googleのインストール含む各種設定を入れます

mix.exs
defmodule Bright.MixProject do

  defp deps do
    [
+     {:ueberauth_google, "~> 0.10"}, 
      {:bcrypt_elixir, "~> 3.0"},
      {:phoenix, "~> 1.7.10"},

config/config.exs
# This file is responsible for configuring your application

import_config "#{config_env()}.exs"

+config :ueberauth, Ueberauth,
+ providers: [
+   google: {Ueberauth.Strategy.Google, []}
+ ]
+
+config :ueberauth, Ueberauth.Strategy.Google.OAuth,
+ client_id: {System, :get_env, ["GOOGLE_CLIENT_ID"]},
+ client_secret: {System, :get_env, ["GOOGLE_CLIENT_SECRET"]}

Überauthが処理するSNS認証(内部的にはOAuth2)を受け付けるコントローラを作り、routerを通します

Überauthでは、ueberauth_authueberauth_failure の2つのハンドラを作るだけでSNS認証実装が済んでしまうので、とても楽チンです

lib/bright_web/controllers/auth_controller.ex
defmodule BrightWeb.OauthController do
  use BrightWeb, :controller
  plug Ueberauth

  alias Ueberauth.Strategy.Helpers

  def request(conn, _params), do: nil

  def callback(%{assigns: %{ueberauth_auth: %Ueberauth.Auth{} = auth}} = conn, _params) do
    conn
    |> put_flash(:info, "Succeed to authenticate!")
    |> put_session(:current_user, auth.uid)
    |> configure_session(renew: true)
    |> redirect(to: "/")
  end

  def callback(%{assigns: %{ueberauth_failure: %Ueberauth.Failure{} = _failure}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: "/")
  end
end
lib/bright_web/router.ex
defmodule BrightWeb.Router do

  pipeline :api do
    plug :accepts, ["json"]
  end

+ pipeline :auth do
+   plug Ueberauth
+ end
+
+ scope "/auth", BrightWeb do
+   pipe_through [:browser, :auth]
+
+   get "/:provider", OauthController, :request
+   get "/:provider/callback", OauthController, :callback
+   post "/:provider/callback", OauthController, :callback
+ end

ここまで準備できたら、インストールと起動を行います

mix deps.get
iex -S mix phx.server

ブラウザで「Googleログイン」ボタンを押下して、下記画面になれば、Elixir側のSNS認証実装は成功です
image.png

OAuthクライアントIDを取得してSNS認証を動かす

上記エラーは、クライアントIDがおかしいことで起こっている訳ですが、それもそのはず、GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET が現時点では空だからです

Googleアカウント認証用のクライアントIDを発行する必要があるので、任意のGoogle CloudアカウントでGoogle Cloudに行き、「APIとサービス」「認証情報」を選択してください
image.png

「+認証情報を作成」から「OAuthクライアントID」を選択します
image.png

下記にように入力します
image.png

作成後クライアントIDのリンクをクリックすると、下記画面が開くので、「クライアントID」と「クライアントシークレット」を控えます
image.png

ターミナルから下記のように設定します

export GOOGLE_CLIENT_ID=【ここにクライアントIDを記入】
export GOOGLE_CLIENT_SECRET=【ここにクライアントシークレットを記入】

上記設定後、再度、ブラウザから「Googleログイン」ボタンを押すと、下記画面に変わるので、Googleアカウントを入力してみてください(すでにGoogleアカウント利用中だとログイン済みアカウントがリストされるのでアカウント選択してください)
image.png

すると、Googleアカウントでログインでき … たように見えて、ログイン済み状態にはなっていません
image.png

それもそのはず、SNS認証を通ったからといって、Elixirで作られたシステムの認証(phx_gen_auth)を通過している訳では無いからです

とは言え、Überauthを使うと、SNS認証の実装がこんなにお手軽にに済むので、とても素敵なライブラリです

この後は?+α(プロダクション開発のリアル)

SNS認証後に、phx_gen_authユーザー登録 or ログインをさせる処理を追加します

つまり、SNS認証から来たユーザーが初回であればユーザー登録し、既に存在しているユーザーであればログインさせます

これの実装は、前回やったScaffoldを使い、GoogleアカウントなどのSNS認証アカウントとphx_gen_authユーザーを紐付けるテーブルを設けます

そして、上記 ueberauth_auth を通る際、phx_gen_authユーザーと紐付いていればログイン、紐付いていなければphx_gen_authユーザー登録させることになります

紐付けるテーブルは、こんな感じでScaffoldします

mix phx.gen.live Accounts UserSocialAuth user_social_auths provider:string identifier:string token:binary display_name:string user_id:references:users

provider には認証プロバイダ(Google/GitHub/Twitterなど)、identifier にはプロバイダから返ってきたuid、token にはプロバイダから返ってきたアクセストークン、display_name にはプロバイダ側で利用しているメールアドレス等、user_id には紐付くphx_gen_authユーザーを入れます

phx_gen_authのユーザー登録やログインは、下記で行っているので、これらを呼び出すか、相当処理を行います(このあたりも余力あればLTなどで共有します)

関数名 処理内容
Accounts.register_user SNS認証後のphx_gen_authユーザー登録
UserAuth.log_in_user SNS認証後のphx_gen_authログイン
Accounts.generate_user_session_token SNS認証後のphx_gen_authトークン発行

あと、デザインをBright「ログイン」に寄せていくなら、前回やったTailwindを活用して近付けてみてください

なおBrightには、この後、2段階認証やユーザー設定からのSNS認証解除、サブメールアドレス追加など、ここで語り尽くせない様々な認証関連機能があるため、実際にプロダクション開発する際は、これら機能の検討や見積も忘れないように注意してください

また、Überauthは単体テストが揃っていないため、プロダクション開発時には、Bright認証の実装者でもある @koyo-miyamura さんの下記コラムをぜひ実践してください

ここまでで習得したスキルは?

さて、Bright「ログイン」そっくりさんを作る過程で、下記のスキルを習得したことになります

  • 「Elixir入門」
    • クラス1:個人で簡単なWeb+DBアプリが作れる
      • mix deps.get
      • Router(router.ex)追加/変更
      • mix phx.gen.live(scaffold)/ブラウザ上CRUD操作
      • scaffold生成.html.heexの.form/.input追加/改修
      • HTML
    • クラス2:Web+DBアプリ開発チームに参画できる
      • config追加/変更
      • ガード節
      • Changeset/バリデーション変更 ※オマケを実施していたら
    • クラス3:サポート有ならWeb+DBチーム開発できる
      • phx_gen_auth
  • 「Webアプリ開発 Elixir」
    • クラス1:零細Web開発をサポート無しでこなせる
      • Uberauth(ソーシャルログイン)

https://bright-fun.org
image.png](https://bright-fun.org

ぜひ下記からBrightを無料ユーザー登録して、スキルパネル「Elixir入門」「Webアプリ開発 Elixir」に、ここまで習得したスキルに「●」をスキル入力してみてください

イマイチ内容とスキルが結びついていない方も、Brightを使いながら、該当スキルを調べたり、復習をしてみてください

p.s.このコラムが、面白かったり、役に立ったら…

image.png にて、どうぞ応援よろしくお願いします :bow:

15
5
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
15
5