今回はPhoenixにGoogle認証を実装します。
SNS認証を扱えるライブラリueberauthを使用して簡単に実装してみます。
プロジェクトの作成
まずは今回のためにPhoenixプロジェクトを作成します。
mix phx.new auth_sample --no-dashboard
cd auth_sample
mix ecto.create
mix phx.server
mix phx.gen.auth コマンドで認証機能を追加
以下のコマンドで認証機能を追加します。
mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.migrate
mix phx.gen.auth
コマンドを使用すると途中「LiveViewを使用するか?」と入力が求められます。今回はy
を入力します。
Googleでログインボタンを追加
Google認証用のボタンを作成します。
ガイドラインにあるアイコンをダウンロードして使用します。
適当にアイコンを選んでpriv/static/images/google_icon.png
としてコピーしておきます。
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
これでボタンができました。
GCPでOAuth2.0の準備
Googleで認証ができるようにGCPを使って準備していきます。
プロジェクトの作成
コンソール画面に入れたら下の画像の通りに進手めてください。
認証情報の作成
上の画像の続き
この後も入力項目がありますが、入力必須の項目だけ入力します。
OAuthクライアントIDの作成
アプリケーションの種類は「ウェブアプリケーション」を選択してください。
承認済みのリダイレクトURLの「URLを追加」をクリックしてhttp://localhost:4000/auth/google/callback
を追加します。
本番環境で使用する場合はhttps://ドメイン/auth/google/callback
を追加します。
発行された「クライアントID」と「クライアントシークレット」を.envファイルを作成して環境変数で登録できるようにしておきます。
2つの値はメモしておきます。
「クライアントID」と「クライアントシークレット」を簡単に環境変数に登録できるように.env
ファイルにしておきます。
export GOOGLE_CLIENT_ID=クライアントID
export GOOGLE_CLIENT_SECRET=クライアントシークレット
.gitignore
ファイルで.env
が選択されないようにしておきます。
...
# 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認証を使用するためのパッケージを追加します。
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 :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認証用のルートを追加します。
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
のアクションを作成していきます。
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
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
作成されたマイグレーションファイルを一部変更します。
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
同じくスキーマファイルも変更します。
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対多の関係で関連付けさせます。
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
テーブルのマイグレーションファイルを一部修正します。
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
テーブルからデータを取得する関数を作成します。
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
テーブルにデータを登録する関数を作成します。
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
を修正していきます。
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
認証で使用するgoogleアカウントを選択したらログインできるようになりました。
ログアウトしてからもう一度Googleでログインしても問題なく機能しています。
最後
ueberauth
を使うことで簡単にSNS認証することができました。
他にもSNS認証があるみたいなので拡張性がすごくあるモジュールです。
ここに乗せたコードはとりあえず動けばいいの精神で書いたので、もし参考にする方はコードをいろいろと変えてみてください。
ここまで読んでいただきありがとうございました。