Pongsatorn Paewsoongnern
Posted on June 17, 2019
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
Posted on June 17, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.