LoginSignup
4
1

PhoenixにGoogle認証を実装する

Posted at

今回はPhoenixにGoogle認証を実装します。
SNS認証を扱えるライブラリueberauthを使用して簡単に実装してみます。

プロジェクトの作成

まずは今回のためにPhoenixプロジェクトを作成します。

mix phx.new auth_sample --no-dashboard
cd auth_sample
mix ecto.create
mix phx.server

スクリーンショット 2024-03-21 200317.png

mix phx.gen.auth コマンドで認証機能を追加

以下のコマンドで認証機能を追加します。

mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.migrate

mix phx.gen.authコマンドを使用すると途中「LiveViewを使用するか?」と入力が求められます。今回はyを入力します。

Googleでログインボタンを追加

スクリーンショット 2024-03-21 202051.png

Google認証用のボタンを作成します。

ガイドラインにあるアイコンをダウンロードして使用します。

適当にアイコンを選んでpriv/static/images/google_icon.pngとしてコピーしておきます。

lib/auth_sample_web/live/user_login_live.ex
defmodule AuthSampleWeb.UserLoginLive do
  use AuthSampleWeb, :live_view

  def render(assigns) do
    ~H"""
...
      </.simple_form>

+      <div class="py-6">
+        <.link href={~p"/auth/google"} class="w-full">
+          <.button class="w-full bg-gray-100 bg-no-repeat bg-contain bg-[url('/images/google_icon.png')] border-2 border-black hover:bg-gray-200 hover:opacity-75">
+            <span class="text-black">Sing in with Google</span>
+          </.button>
+        </.link>
+      </div>
    </div>
    """
  end
...
end

これでボタンができました。

スクリーンショット 2024-03-21 204105.png

GCPでOAuth2.0の準備

Googleで認証ができるようにGCPを使って準備していきます。

プロジェクトの作成

コンソール画面に入れたら下の画像の通りに進手めてください。

スクリーンショット 2024-03-21 204707.png

スクリーンショット 2024-03-21 204716.png

スクリーンショット 2024-03-21 204741.png

認証情報の作成

スクリーンショット 2024-03-21 204821.png

スクリーンショット 2024-03-21 204835.png

スクリーンショット 2024-03-21 204850.png

スクリーンショット 2024-03-21 204901.png

スクリーンショット 2024-03-21 204930.png

スクリーンショット 2024-03-21 205032.png

上の画像の続き

スクリーンショット 2024-03-21 205037.png

この後も入力項目がありますが、入力必須の項目だけ入力します。

OAuthクライアントIDの作成

スクリーンショット 2024-03-21 205238.png

スクリーンショット 2024-03-21 205248.png

スクリーンショット 2024-03-21 205355.png

アプリケーションの種類は「ウェブアプリケーション」を選択してください。

承認済みのリダイレクトURLの「URLを追加」をクリックしてhttp://localhost:4000/auth/google/callbackを追加します。
本番環境で使用する場合はhttps://ドメイン/auth/google/callbackを追加します。

スクリーンショット 2024-03-21 205430.png

発行された「クライアントID」と「クライアントシークレット」を.envファイルを作成して環境変数で登録できるようにしておきます。
2つの値はメモしておきます。

「クライアントID」と「クライアントシークレット」を簡単に環境変数に登録できるように.envファイルにしておきます。

.env
export GOOGLE_CLIENT_ID=クライアントID
export GOOGLE_CLIENT_SECRET=クライアントシークレット

.gitignoreファイルで.envが選択されないようにしておきます。

.gitignore
...

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

+ .env

以下のコマンドを実行して環境変数を登録します。

source .env

Google認証を実装

GCPでの設定が終わったのでPHoenixプロジェクトにGoogle認証機能を追加していきます。

サンプルが用意されているのでこちらを参考にします。

ueberauth_google パッケージを追加

Google認証を使用するためのパッケージを追加します。

mix.exs
defmodule AuthSample.MixProject do
...
  defp deps do
    [
      ...
      {:plug_cowboy, "~> 2.5"},
+      {:ueberauth_google, "~> 0.12.1"}
    ]
  end
...
end

パッケージを以下のコマンドで取得します。

mix deps.get

uebrauthのconfig設定

uebrauth用のコンフィグを追加します。

config/config.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"]}

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"

ルーティングの設定

Google認証用のルートを追加します。

lib/auth_sample_web/router.ex
defmodule AuthSampleWeb.Router do
...
+   scope "/auth", AuthSampleWeb do
+     pipe_through :browser
+
+     get "/:provider", AuthController, :request
+     get "/:provider/callback", AuthController, :callback
+     post "/:provider/callback", AuthController, :callback
+   end
 ...
end

コントローラーの実装

AuthControllerのアクションを作成していきます。

lib/auth_sample_web/controllers/auth_controller.ex
defmodule AuthSampleWeb.AuthController do
  use AuthSampleWeb, :controller
  plug Ueberauth

  def request(conn, _params), do: nil

  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: ~p"/users/log_in")
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    IO.inspect(auth, label: "response auth")
    
    conn
    |> put_flash(:info, "Successfully authenticated.")
    |> put_session(:current_user, auth.uid)
    |> configure_session(renew: true)
    |> redirect(to: ~p"/")
  end
end

デバッグようにIO.inspect(auth, label: "response auth")を追加しています。

サーバーを立ち上げてhttp://localhost:4000/users/log_inにアクセスしてGoogle認証ボタンを押してみましょう。
http://localhost:4000/users/log_in

mix phx.server

スクリーンショット 2024-03-21 223126.png

スクリーンショット 2024-03-21 224359.png

スクリーンショット 2024-03-21 224436.png

スクリーンショット 2024-03-21 224443.png

IO.inspect/2の出力は以下

response auth: %Ueberauth.Auth{
  uid: "googleアカウントのuid",
  provider: :google,
  strategy: Ueberauth.Strategy.Google,
  info: %Ueberauth.Auth.Info{
    name: nil,
    first_name: nil,
    last_name: nil,
    nickname: nil,
    email: "googleアカウントのメールアドレス",
    location: nil,
    description: nil,
    image: "画像のurl",
    phone: nil,
    birthday: nil,
    urls: %{profile: nil, website: nil}
  },
  credentials: %Ueberauth.Auth.Credentials{
    token: "****",
    refresh_token: nil,
    token_type: "Bearer",
    secret: nil,
    expires: true,
    expires_at: 1711032276,
    scopes: ["openid", "https://www.googleapis.com/auth/userinfo.email"],
    other: %{}
  },
  extra: %Ueberauth.Auth.Extra{
    raw_info: %{
      user: %{
        "email" => "googleアカウントのメールアドレス",
        "email_verified" => true,
        "picture" => "画像のurl",
        "sub" => "*****"
      },
      token: %OAuth2.AccessToken{
        access_token: "*****",
        refresh_token: nil,
        expires_at: 1711032276,
        token_type: "Bearer",
        other_params: %{
          "id_token" => "******",
          "scope" => "openid https://www.googleapis.com/auth/userinfo.email"
        }
      }
    }
  }
}

sns_authsテーブルの作成

Google認証で受け取った値を活用してユーザー登録をするためにsns_authsテーブルを作成します。

mix phx.gen.schema Accounts.SnsAuth sns_auths provider:string uid:string user_id:references:users

作成されたマイグレーションファイルを一部変更します。

priv/repo/migrations/タイムスタンプ_create_sns_auths.exs
defmodule AuthSample.Repo.Migrations.CreateSnsAuths do
  use Ecto.Migration

  def change do
    create table(:sns_auths) do
-     add :provider, :string
-     add :uid, :string
-     add :user_id, references(:users, on_delete: :nothing)
+     add :provider, :string, null: false
+     add :uid, :string, null: false
+     add :user_id, references(:users, on_delete: :delete_all), null: false

      timestamps(type: :utc_datetime)
    end

    create index(:sns_auths, [:user_id])
  end
end

同じくスキーマファイルも変更します。

lib\auth_sample\accounts\sns_auth.ex
defmodule AuthSample.Accounts.SnsAuth do
  use Ecto.Schema
  import Ecto.Changeset
+ alias AuthSample.Accounts.User

  schema "sns_auths" do
    field :provider, :string
    field :uid, :string
-    field :user_id, :id
+    belongs_to :user, User

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(sns_auth, attrs) do
    sns_auth
    |> cast(attrs, [:provider, :uid])
    |> validate_required([:provider, :uid])
  end
end

AuthSample.Accounts.Userモジュールのスキーマにsns_authsテーブルのスキーマを関連付けさせます。

今回、SNS認証の中ではGoogle認証しか使用していませんが、ほかのSNS認証を追加する際に拡張性がいいようにhas_manyで1対多の関係で関連付けさせます。

lib/auth_sample/accounts/user.ex
defmodule AuthSample.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
+ alias AuthSample.Accounts.SnsAuth

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :naive_datetime
+   has_many :sns_auths, SnsAuth

    timestamps(type: :utc_datetime)
  end
...
end

また、Google認証でアカウントを登録する場合はパスワードが必要ありません。そのため、usersテーブルのマイグレーションファイルを一部修正します。

defmodule AuthSample.Repo.Migrations.CreateUsersAuthTables do
  use Ecto.Migration

  def change do
    execute "CREATE EXTENSION IF NOT EXISTS citext", ""

    create table(:users) do
      add :email, :citext, null: false
-      add :hashed_password, :string, null: false
+      add :hashed_password, :string
      add :confirmed_at, :naive_datetime
      timestamps(type: :utc_datetime)
    end

    create unique_index(:users, [:email])

    create table(:users_tokens) do
      add :user_id, references(:users, on_delete: :delete_all), null: false
      add :token, :binary, null: false
      add :context, :string, null: false
      add :sent_to, :string
      timestamps(updated_at: false)
    end

    create index(:users_tokens, [:user_id])
    create unique_index(:users_tokens, [:context, :token])
  end
end

以下のコマンドでdbをリセットします。

mix ecto.reset

アカウントの登録~ログインまでの機能を実装

まず、Google認証でアカウントを作成しているかをチェックするためにuidの値でsns_authsテーブルからデータを取得する関数を作成します。

lib/auth_sample/accounts.ex
defmodule AuthSample.Accounts do
  @moduledoc """
  The Accounts context.
  """

  import Ecto.Query, warn: false
  alias AuthSample.Repo

-  alias AuthSample.Accounts.{User, UserToken, UserNotifier}
+  alias AuthSample.Accounts.{User, UserToken, UserNotifier, SnsAuth}

  ## Database getters

...
  @doc """
  Gets a single user.

  Raises `Ecto.NoResultsError` if the User does not exist.

  ## Examples

      iex> get_user!(123)
      %User{}

      iex> get_user!(456)
      ** (Ecto.NoResultsError)

  """
  def get_user!(id), do: Repo.get!(User, id)

+ def get_sns_auth(uid), do: Repo.get_by(SnsAuth, uid: uid)
...
end

sns_authsテーブルにデータを登録する関数を作成します。

lib/auth_sample/accounts.ex
defmodule AuthSample.Accounts do
  @moduledoc """
  The Accounts context.
  """

  import Ecto.Query, warn: false
  alias AuthSample.Repo

  alias AuthSample.Accounts.{User, UserToken, UserNotifier, SnsAuth}
+ alias Ueberauth.Auth
...
  ## User registration

  @doc """
  Registers a user.

  ## Examples

      iex> register_user(%{field: value})
      {:ok, %User{}}

      iex> register_user(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
+  def register_user(%Auth{} = auth) do
+    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
+    Ecto.Multi.new()
+    |> Ecto.Multi.insert(:user, %User{email: auth.info.email, confirmed_at: now})
+    |> Ecto.Multi.insert(:sns_auth, fn %{user: %User{id: user_id}} ->
+      %SnsAuth{provider: auth.provider, uid: auth.uid, user_id: user_id}
+    end)
+    |> Repo.transaction()
+  end

  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end
...
+  ## SNSAuth
+
+  def create_sns_auth(%User{id: user_id}, %Auth{} = auth) do
+    Repo.insert(%SnsAuth{provider: auth.provider, uid: auth.uid, user_id: user_id})
+  end
end

とりあえず、いろいろ考慮するべきことがありますが、必要な機能はそろいました。

続いてAuthControllerを修正していきます。

lib/auth_sample_web/controllers/auth_controller.ex
defmodule AuthSampleWeb.AuthController do
  use AuthSampleWeb, :controller
  plug Ueberauth
+  alias AuthSample.Accounts
+  alias AuthSample.Accounts.{User, SnsAuth}
+  alias AuthSampleWeb.UserAuth

  def request(conn, _params), do: nil

  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: ~p"/users/log_in")
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
-    IO.inspect(auth, label: "response auth")
-
-    conn
-    |> put_flash(:info, "Successfully authenticated.")
-    |> put_session(:current_user, auth.uid)
-    |> configure_session(renew: true)
-    |> redirect(to: ~p"/")
+   user = Accounts.get_user_by_email(auth.info.email)
+    sns_auth = Accounts.get_sns_auth(auth.uid)
+    log_in_user(conn, user, sns_auth, auth)
  end
+
+  defp log_in_user(conn, %User{} = user, %SnsAuth{}, _auth) do
+    UserAuth.log_in_user(conn, user, %{"remember_me" => "true"})
+  end
+
+  defp log_in_user(conn, %User{} = user, nil, auth) do
+    case Accounts.create_sns_auth(user, auth) do
+      {:ok, %SnsAuth{} = sns_auth} ->
+        log_in_user(conn, user, sns_auth, auth)
+
+      {:error, _cs} ->
+        conn
+        |> put_flash(:error, "Failed to authenticate.")
+        |> redirect(to: ~p"/users/log_in")
+    end
+  end
+
+  defp log_in_user(conn, _user, _sns_auth, auth) do
+    case Accounts.register_user(auth) do
+      {:ok, %{user: user, sns_auth: sns_auth}} ->
+        log_in_user(conn, user, sns_auth, auth)
+
+      {:error, _, _cs, _} ->
+        conn
+        |> put_flash(:error, "Failed to authenticate.")
+        |> redirect(to: ~p"/users/log_in")
+    end
+  end
end

http://localhost:4000/users/log_inにアクセスして実際にログインできるか確認しましょう。
http://locahost:4000/users/log_in

mix phx.server

スクリーンショット 2024-03-22 005815.png

認証で使用するgoogleアカウントを選択したらログインできるようになりました。

スクリーンショット 2024-03-22 010334.png

psqlでデータを確認してらちゃんとデータが入っています。
スクリーンショット 2024-03-22 010953.png

ログアウトしてからもう一度Googleでログインしても問題なく機能しています。

最後

ueberauthを使うことで簡単にSNS認証することができました。
他にもSNS認証があるみたいなので拡張性がすごくあるモジュールです。

ここに乗せたコードはとりあえず動けばいいの精神で書いたので、もし参考にする方はコードをいろいろと変えてみてください。

ここまで読んでいただきありがとうございました。:thumbsup:

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