DEV Community

Cover image for ทำ Social Login แบบง่ายๆบน Phoenix ด้วย Ueberauth
Pongsatorn Paewsoongnern for miraiverse

Posted on

ทำ Social Login แบบง่ายๆบน Phoenix ด้วย Ueberauth

Introduction

หลายคนคงอยากจะเริ่มใช้ Phoenix Framework ในการทำ Web App สักตัวหนึ่ง แต่ติดปัญหาว่าเราจะเริ่มทำระบบ Login ยังไงดีน้า~

ผมเลยจะขอแนะนำการทำ Social Login แบบง่ายๆ(อีกละ) สำหรับ Web App ของเราโดยใช้ ueberauth

ก่อนอื่นเลย มาแนะนำกันก่อนว่าเจ้า ueberauth เนี่ยเป็น library ที่ช่วยเราในการทำ Authentication สำหรับ Plug-based Web Application ซึ่งก็คือ Phoenix Framework ที่ใช้กันแหละ โดยมันได้มี Providers ให้เราได้เลือกใช้เยอะแยะมาก เช่น

Noted: ในเคสนี้ถือว่าทุกคนคงพอคุ้นเคยกับตัว Phoenix Framework มาพอสมควรแล้วนะ เพราะจะได้ลงรายละเอียดแค่ในส่วนของการทำ Login


Add deps!

อ๊ะ เริ่มจากการเพิ่ม ueberauth เข้าไปเป็น dependencies ในโปรเจคก่อยยย~

# mix.exs
defp deps do
  [{:ueberauth, "~> 0.6.1"}]
end
Enter fullscreen mode Exit fullscreen mode

แต่เดี๋ยวก่อน! ไหนๆก็ไหนๆแล้ว เพิ่ม dependencies สำหรับ provider ที่เราต้องการไปด้วยเลยดีกว่า ในที่นี้ผมขอเลือกใช้ facebook provider นะครัช มันเลยจะกลายเป็น

# mix.exs
defp deps do
  [
    {:ueberauth, "~> 0.6.1"},
    {:ueberauth_facebook, "~> 0.8.0"}
  ]
end
Enter fullscreen mode Exit fullscreen mode

เสร็จแล้วอย่าลืมสั่ง 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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

ฟังชั่น 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
Enter fullscreen mode Exit fullscreen mode

เพียงเท่านี้ เราก็พร้อมที่จะเริ่มใช้งานตัว 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
Enter fullscreen mode Exit fullscreen mode

วิธีการนำ 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
Enter fullscreen mode Exit fullscreen mode

อ๊ะ ทีนี้ก็เป็นอันเสร็จสมบูรณ์ เราสามารถเรียกใช้ข้อมูลของ 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>
Enter fullscreen mode Exit fullscreen mode

Cover Image by Gerd Altmann from Pixabay

Top comments (0)