ทำ Social Login แบบง่ายๆบน Phoenix ด้วย Ueberauth
Tags: elixir, phoenix, oauth, ueberauth
Published: 2019-06-17 04:14:38

---
title: ทำ Social Login แบบง่ายๆบน Phoenix ด้วย Ueberauth
published: true
description:
tags: elixir, phoenix, oauth, ueberauth
cover_image: https://thepracticaldev.s3.amazonaws.com/i/gto2d6iqq7biw6tqquhv.jpg
---

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

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

ก่อนอื่นเลย มาแนะนำกันก่อนว่าเจ้า `ueberauth` เนี่ยเป็น library ที่ช่วยเราในการทำ Authentication สำหรับ Plug-based Web Application ซึ่งก็คือ Phoenix Framework ที่ใช้กันแหละ โดยมันได้มี Providers ให้เราได้เลือกใช้เยอะแยะมาก เช่น
- Facebook,
- Twitter
- Google
- GitHub
- [และอื่นๆ](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies)

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

---

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

```elixir
# mix.exs
defp deps do
  [{:ueberauth, "~> 0.6.1"}]
end
```

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

```elixir
# 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

```elixir
# 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 กัน

```elixir
# 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 กัน

```elixir
# 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`

```elixir
# 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 นั่นเอง~

```elixir
# 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 ของเราได้แล้วละ~~

```html
<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](https://pixabay.com/users/geralt-9301)