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

この記事は、[Elixir Advent Calendar 2023 シリーズ14](https://qiita.com/advent-calendar/2023/elixir) の16日目です

---

[piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:

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

https://bright-fun.org

[前回は、Elixir Web CRUDの基本として、LiveView Scaffold＋Tailwindで「スキルパネル」そっくりさん（下記赤枠）を爆速開発](https://qiita.com/piacerex/items/c35c63ac45c79baa7371) する内容でした
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/c7809dd5-23b2-b3e1-b35e-5c34d38bdcba.png)](https://qiita.com/piacerex/items/c35c63ac45c79baa7371)

今回は、Phoenix標準の独自ID認証機能「phx_gen_auth」と、SNS認証（Google／GitHub／Twitterなどのアカウントでの認証）を叶えるライブラリ「Überauth」で、Bright「ログイン」そっくりさんを作ってみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5f683473-8698-4d73-9ed6-ee2c6475c0f8.png)

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

https://qiita.com/piacerex/items/bac30ec027c9eef0e717

あと、このコラムが、面白かったり、役に立ったら、![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/907a8695-4d83-1b5c-8052-d0ce52de5f66.png) をお願いします :bow:

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

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

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

今回は、前回作ったPhoenix PJに機能追加していく形で進行しますので、作成が未だであれば、[前回コラムをお試しいただいて](https://qiita.com/piacerex/items/c35c63ac45c79baa7371) から、本コラムに戻って下記を実施してください

## 認証をScaffold自動生成

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

```bash
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/fb4a6055-3b57-cc55-96dc-5f6bf6368a55.png)

入力し、ボタン押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a63e380d-939b-06ff-7483-ff55d66226f6.png)

ユーザー登録が行われ、ログイン済み状態に右上リンクが変わるので、「Log out」し、「Log in」リンクを押下してみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/4e1920e7-bfcd-3023-71af-970f2c2a6d86.png)

先ほど作成したユーザーでログインできることが確認できると思います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/1b231d40-a7b5-7b0f-b356-be07adc25eef.png)

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

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

上記で、ユーザー登録時にメールで確認を行いませんでしたが、実は確認のためのURLが載ったメールは投げられており、ブラウザで ` http://localhost:4000/dev/mailbox` をご覧ください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/eae1d808-40ec-d329-7e76-47ced5cf422f.png)

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

https://qiita.com/piacerex/items/35dc7ee8129b9a1deb9d#%EF%BC%93%E3%83%A1%E3%83%BC%E3%83%AB%E9%80%81%E4%BF%A1%E8%A8%AD%E5%AE%9A

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

```diff_elixir: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
…
```

```diff_elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/da9c3273-48e9-af1d-bd2d-766fbe3f2c1f.png)

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

```diff_elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/110b06c2-9cc6-1d1f-dde2-53a764824baf.png)

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

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

https://qiita.com/piacerex/items/5f11e33db748830c7636

# 「Überauth」によるSNS認証

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

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

https://github.com/ueberauth/ueberauth_google

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

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

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

https://qiita.com/piacerex/items/8577f0e9a91556feb97c#liveview%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E6%A9%9F%E8%83%BD%E3%81%A7livebook%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%B8%8A%E3%81%8B%E3%82%89%E7%89%B9%E5%AE%9A

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/24d1d68c-e160-8372-c30e-29ee25974947.png)

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

```diff_elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/e94c763a-9cd9-0f0b-ae05-13bd564967a5.png)

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

## Überauthの導入

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

```diff_elixir:mix.exs
defmodule Bright.MixProject do
…
  defp deps do
    [
+     {:ueberauth_google, "~> 0.10"}, 
      {:bcrypt_elixir, "~> 3.0"},
      {:phoenix, "~> 1.7.10"},
…
```

```diff_elixir: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_auth` と `ueberauth_failure` の2つのハンドラを作るだけでSNS認証実装が済んでしまうので、とても楽チンです

```diff_elixir: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
```

```diff_elixir: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
…
```

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

```bash
mix deps.get
iex -S mix phx.server
```

ブラウザで「Googleログイン」ボタンを押下して、下記画面になれば、Elixir側のSNS認証実装は成功です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/2780848d-ab2a-19eb-0003-33f1ce3d15e8.png)

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

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

Googleアカウント認証用のクライアントIDを発行する必要があるので、任意のGoogle CloudアカウントでGoogle Cloudに行き、「APIとサービス」「認証情報」を選択してください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/b8a62d04-7d2c-0ac5-ffc6-256595d39bf1.png)

「＋認証情報を作成」から「OAuthクライアントID」を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/1f00dcaf-e961-ad46-fac7-e3362a07a177.png)

下記にように入力します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/42b5f96e-e198-7276-10db-81a741cf30b9.png)

作成後クライアントIDのリンクをクリックすると、下記画面が開くので、「クライアントID」と「クライアントシークレット」を控えます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6f8daa09-0beb-8f95-ee54-9049ce72cc29.png)

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

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

上記設定後、再度、ブラウザから「Googleログイン」ボタンを押すと、下記画面に変わるので、Googleアカウントを入力してみてください（すでにGoogleアカウント利用中だとログイン済みアカウントがリストされるのでアカウント選択してください）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/d46f6cc3-29d1-dd27-ee4d-697b2157327b.png)

すると、Googleアカウントでログインでき … たように見えて、ログイン済み状態にはなっていません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a15ee56b-6661-6bbc-ce1e-a4f8e88bdf0d.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します

```bash
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 さんの下記コラムをぜひ実践してください

https://qiita.com/koyo-miyamura/items/173d61e23c7a38d0826b

# ここまでで習得したスキルは？

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

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

https://bright-fun.org
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f6d76f4e-08dd-f832-239a-20ae2f170f2a.png)](https://bright-fun.org

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

https://bright-fun.org/

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/907a8695-4d83-1b5c-8052-d0ce52de5f66.png) にて、どうぞ応援よろしくお願いします :bow: