Introduction
หลายคนคงอยากจะเริ่มใช้ Phoenix Framework ในการทำ Web App สักตัวหนึ่ง แต่ติดปัญหาว่าเราจะเริ่มทำระบบ Login ยังไงดีน้า~
ผมเลยจะขอแนะนำการทำ Social Login แบบง่ายๆ(อีกละ) สำหรับ Web App ของเราโดยใช้ ueberauth
ก่อนอื่นเลย มาแนะนำกันก่อนว่าเจ้า ueberauth
เนี่ยเป็น library ที่ช่วยเราในการทำ Authentication สำหรับ Plug-based Web Application ซึ่งก็คือ Phoenix Framework ที่ใช้กันแหละ โดยมันได้มี Providers ให้เราได้เลือกใช้เยอะแยะมาก เช่น
- Facebook,
- GitHub
- และอื่นๆ
Noted: ในเคสนี้ถือว่าทุกคนคงพอคุ้นเคยกับตัว Phoenix Framework มาพอสมควรแล้วนะ เพราะจะได้ลงรายละเอียดแค่ในส่วนของการทำ Login
Add deps!
อ๊ะ เริ่มจากการเพิ่ม ueberauth
เข้าไปเป็น dependencies ในโปรเจคก่อยยย~
# mix.exs
defp deps do
[{:ueberauth, "~> 0.6.1"}]
end
แต่เดี๋ยวก่อน! ไหนๆก็ไหนๆแล้ว เพิ่ม dependencies สำหรับ provider ที่เราต้องการไปด้วยเลยดีกว่า ในที่นี้ผมขอเลือกใช้ facebook provider
นะครัช มันเลยจะกลายเป็น
# mix.exs
defp deps do
[
{:ueberauth, "~> 0.6.1"},
{:ueberauth_facebook, "~> 0.8.0"}
]
end
เสร็จแล้วอย่าลืมสั่ง mix deps.get
กันด้วยนะฮัฟ~
Configuration
เอาหล่ะ ตอนนี้เราก็จะมาเพิ่ม config ให้เจ้า ueberauth กันว่าจะใช้ provider อะไรบ้าง และ credential ต่างๆสำหรับมัน เช่น client id และ client secret
# config/config.exs
config :ueberauth, Ueberauth,
providers: [
facebook: {Ueberauth.Strategy.Facebook, []}
]
# config/dev.exs
config :ueberauth, Ueberauth.Strategy.Facebook.OAuth,
client_id: System.get_env("FACEBOOK_CLIENT_ID"),
client_secret: System.get_env("FACEBOOK_CLIENT_SECRET")
Noted: สามารถหา client id และ client secret ได้จาก developer console ของ Facebook
Router & Controller
หลังจาก config เสร็จแล้ว เราก็มาสร้าง controller และจัดการ route สำหรับ login กัน
# controllers/auth_controller.ex
defmodule NorzeWeb.AuthController do
@moduledoc false
use NorzeWeb, :controller
plug Ueberauth
alias Norze.Accounts
def delete(conn, _params) do
conn
|> configure_session(drop: true)
|> redirect(to: "/")
end
def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
conn
|> put_flash(:error, "Failed to authenticate.")
|> redirect(to: "/")
end
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
case Accounts.from_auth(auth) do
{:ok, user} ->
conn
|> put_session(:current_user_id, user.id)
|> configure_session(renew: true)
|> redirect(to: "/")
{:error, _} ->
conn
|> put_flash(:error, "Failed authenticated.")
|> redirect(to: "/")
end
end
end
ฟังชั่น callback มีหน้าที่รับ request หลังจากตัว ueberauth ทำการ authen user เสร็จเรียบร้อยแล้ว โดยจะส่งข้อมูลต่างๆกลับมาให้เรา เช่น uid, provider และ profile ของ user เอง
โดยจากที่เห็นคือเราจะมี callback อยู่ 2 อัน สำหรับเคสที่ success และ fail
สำหรับฟังชั่น delete นั้น ใช้แค่ลบ session เวลา logout เฉยๆ
อ๊ะ ต่อไปเราก็เอา controller เราไปใส่เพิ่มใน route กัน
# router.ex
scope "/auth", NorzeWeb do
pipe_through :browser
get "/logout", AuthController, :delete
get "/:provider", AuthController, :request
get "/:provider/callback", AuthController, :callback
end
เพียงเท่านี้ เราก็พร้อมที่จะเริ่มใช้งานตัว social login แล้วละ!
ปล. จะเห็นว่า /:provider
วิ่งไปยังฟังชั่น :delete
ซึ่งเราไม่ได้เขียน ไม่ต้องตกใจครับ เป็น default ของตัว plug Ueberauth นั่นเอง
Let's try
อ๊ะ มาลองกันดีกว่า
-> http://localhost:4000/auth/facebook
เมื่อเข้าลิงก์นี้ไป เราก็จะถูก ueberauth พาไปหน้า authen ของแต่ละ provider และเมื่อดำเนินการเสร็จแล้ว เราก็จะถูกพากลับมายัง callback_url
ของเรานั่นเอง
เป็นอันเสร็จพิธี!
Bonus track
จากการเขียน callback ฟังชั่นของเราด้านบน เราจะทำการเก็บ user_id ใส่ session ไว้เมื่อเข้าสู่ระบบสำเร็จ แต่คำถามคือ เราจะเอาค่านี้ไปใช้ต่อยังไง สมมติเราอยากเรียกใช้ user_email ในตัว template
ไม่ยากเลย เพียงแค่สร้าง plug
ขึ้นมาอีกตัวนึงให้ทุก route หลักของ web app เราวิ่งผ่าน
โดยตัว plug นี้จะทำหน้าที่เช็คค่า user_id ใน session ว่ามีไหม
ถ้ามี -> ไปถึงข้อมูล user มาแล้วยัดใส่ conn.assigns
ไว้ พร้อมเซ็ต user_signed_in?
เป็น true
ถ้าไม่มี -> เซ็ต user_signed_in?
เป็น false
โดยให้ข้อมูล user เป็น nil
# plugs/authentication.ex
defmodule NorzeWeb.Plugs.SetCurrentUser do
@moduledoc false
import Plug.Conn
alias Norze.Accounts
def init(_params) do
end
def call(conn, _params) do
user_id = Plug.Conn.get_session(conn, :current_user_id)
if current_user = user_id && Accounts.get_user(user_id) do
conn
|> assign(:current_user, current_user)
|> assign(:user_signed_in?, true)
else
conn
|> assign(:current_user, nil)
|> assign(:user_signed_in?, false)
end
end
end
วิธีการนำ plug ไปใช้ใน route ก็เพียงแค่เพิ่มของเราเข้าไปยัง pipeline นั่นเอง~
# router.ex
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug Plugs.SetCurrentUser
end
อ๊ะ ทีนี้ก็เป็นอันเสร็จสมบูรณ์ เราสามารถเรียกใช้ข้อมูลของ user ภายใน template ของเราได้แล้วละ~~
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<%= if @conn.assigns.user_signed_in? do %>
<a href="#" class="button is-info">
<strong><%= @conn.assigns.current_user.email %></strong>
</a>
<%= link gettext("logout"), to: Routes.auth_path(@conn, :delete), class: "button is-light" %>
<% else %>
<a href="<%= Routes.auth_path(@conn, :request, "facebook") %>" class="button is-link">
<strong><%= gettext("login with facebook") %></strong>
</a>
<% end %>
</div>
</div>
</div>
Cover Image by Gerd Altmann from Pixabay
Top comments (0)