Phoenix

Rehber: Phoenix API uygulaması #2

6
murat

İlk bölümünü şurada yayınladığım serisinin ikinci yazısıdır.

Bir önceki bölümde basitçe crud işlemleri yapan bir endpoint hazırlamıştık. Tabi ki sırada bir API ın olmazsa olmazı authentication işlemlerini nasıl yapacağımızdan bahsedeceğim.

Öncelikle ihtiyaç duyacağımız paketlerden bahsedelim. Ruby yazanlar ve Rails ile uygulama geliştirenler bilirler plataformatec'in geliştirdiği ve José Valim abimizin, yani Elixir dilinin yaratıcısının çok büyük katkıları olan devise adında bir gem var ki authentication adına ne var ise her şeyi 3-5 generator komutu ile saniyeler içerisinde halledebiliyorsunuz. Elixir topluluğu için geliştirilen coherence adında bir devise alternatifi olsa da ben authentication işlemleri için yaygın olarak guardian paketini kullanmayı tercih ediyorum. Bu bölümde de guardian ile basit bir authentication nasıl yapılır bundan bahsedeceğim.

Paketleri deps fonksiyonuna ekleyerek başlayalım.

defp deps do
  # bcrypt eklemiştik ama altını tekrar çizelim...
  {:bcrypt_elixir, "~>  2.0"},
  # ...
  {:guardian, "~> 1.2"}
  # ...
end

mix deps.get diyerek guardian'ı projemize ekliyoruz. Dileyen şuraya da bakabilir.

Guardian bize JWT token üretme imkanı tanıyacak, bunun için ufak bir konfigürasyon yapmakta fayda var. config/config.exs dosyamıza aşağıdaki konfigürasyonu ekleyelim.

# ...
config :app, App.Auth.Guardian,
  issuer: "app",
  secret_key: System.get_env("GUARDIAN_SECRET_KEY")
# ...

Not App.Auth.Guardian modülünü birazdan oluşturacağız.

router.ex dosyamızda gerekli değişiklikleri yapalım...

pipeline :ensure_auth do
  plug App.Auth.Guardian.Pipeline
end

scope "/api", AppWeb do
  pipe_through :api

  scope "/auth", Auth do
    resources "/sign_up", RegisterController, only: [:create]
    resources "/sign_in", SessionController, only: [:create]
  end
end

scope "/api", AppWeb do
  pipe_through [:api, :ensure_auth]

  scope "/v1", V1 do
    resources "/users", UserController, except: [:new, :edit]
  end
end

Bir önceki yazıda, resources "/users" resource'unu sadece api pipeline'ından geçirmiştik, şimdi bu resource rotasını yine /api scope'u içinde kalarak pipe_through [:api, :ensure_auth] pipeline'larından geçireceğiz. :ensure_auth pipeline'ında App.Auth.Guardian.Pipeline adında bir plug'ın varlığından bahsettik. lib/app/auth/guardian/pipeline.ex path'inde aynı isimle bir module tanımlayarak içeriğini aşağıdaki gibi yapalım.

defmodule App.Auth.Guardian.Pipeline do
  use Guardian.Plug.Pipeline, otp_app: :app,
                              module: App.Auth.Guardian,
                              error_handler: App.Auth.Guardian.ErrorHandler

  # Requestlerde authorization header arayıp, token bir access token ise doğrula
  plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
  plug Guardian.Plug.EnsureAuthenticated
end

Bu oluşturduğumuz modül Guardian.Plug.Pipeline modülünü konfigüre eder. Yukarıdaki modülde görüldüğü gibi iki modüle daha ihtiyacı var, birincisi App.Auth.Guardian ikincisi App.Auth.Guardian.ErrorHandler, bu modülleri de oluşturalım ve içlerini aşağıdaki gibi dolduralım.

# lib/app/auth/guardian.ex
defmodule App.Auth.Guardian do
  use Guardian, otp_app: :app

  alias App.Auth

  def subject_for_token(resource, _claims), do: {:ok, to_string(resource.id)}

  def resource_from_claims(claims) do
    Auth.get_user!(claims["sub"])
  end
end

# lib/app/auth/guardian/error_handler.ex
defmodule App.Auth.Guardian.ErrorHandler do
  import Plug.Conn

  @behaviour Guardian.Plug.ErrorHandler
  @impl Guardian.Plug.ErrorHandler
  def auth_error(conn, {type, _reason}, _opts) do
    body = Poison.encode!(%{meta: %{status: to_string(type)}})

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(401, body)
  end
end

App.Auth.Guardian modülündeki resource_from_claims/1 fonksiyonu claims map'i içinde "sub" key'i ile kullanıcımızın id sini tutacak. Bu id ile Auth modülünde get_user!/1 fonksiyonunu çağırarak giriş yapmış kullanıcının veritabanı kaydını bulacağız. ErrorHandler modülümüz ise authentication hatalarını alıp 401 http statüsü ile cevap verecek.

Şimdi SessionController modülümüzü oluşturalım.

defmodule AppWeb.Auth.SessionController do
  use AppWeb, :controller
  alias App.Auth

  action_fallback AppWeb.FallbackController

  def create(conn, %{"user" => %{"email" => email, "password" => password}}) do
    case Auth.authenticate_user(email, password) do
      {:ok, token, _claims}->
        conn
        |> put_view(AppWeb.AuthView)
        |> render("token.json", token: token)

      {:error, message} ->
        conn
        |> put_status(:unauthorized)
        |> put_view(AppWeb.AuthView)
        |> render("error.json", %{message: to_string(message)})
    end
  end
end

SessionController.create/2 fonksiyonu parametre olarka gelen email ve password ile Auth.authenticate_user/2 fonksiyonunu çalıştırıp başarılı ve ya hatalı durumlara göre malum cevapları verecektir. Şimdi Auth modülünde authenticate_user/2 fonksiyonumuzu yazmamız gerekiyor.

# lib/app/auth.ex
defmodule App.Auth do
  # ...

  def authenticate_user(email, password) do
    case Repo.one(from(u in User, where: u.email == ^email)) do
      nil ->
        {:error, :invalid_credentials}
      user ->
        if Bcrypt.verify_pass(password, user.password_digest) do
          App.Auth.Guardian.encode_and_sign(user)
        else
          {:error, :invalid_credentials}
        end
    end
  end
end

Bu fonksiyon ise verilen email ile veritabanında kullanıcımızı arar, bulursa ve Bcrypt.verify_pass/2 fonksiyonu ile verilen password ile veritabanındaki hashlenmiş passwordü test eder, verilen bilgiler doğru ise Guardian.encode_and_sign fonksiyonu ile bir JWT token ile birlikte claims oluşturup döndürür, bulamazsa ve ya password yanlış ise {:error, :invalid_credentials} şeklinde bir tuple döndürür.

SessionController'a dönersek response için AuthView kullanmıştık. AuthView modülümüzü de aşağıdaki gibi oluşturalım.

# lib/app_web/views/auth_view.ex

defmodule AppWeb.AuthView do
  use AppWeb, :view

  def render("token.json", %{token: token} = _) do
    %{token: token}
  end

  def render("error.json", %{message: message}) do
    %{error: message}
  end
end

Sonuçlar

Şimdi localhost:4000/api/v1/users adresine get requesti attığımızda aşağıdaki gibi bir cevap almamız gerekiyor.

➜ http :4000/api/v1/users | jq
HTTP/1.1 401 Unauthorized
cache-control: max-age=0, private, must-revalidate
content-length: 37
content-type: application/json; charset=utf-8
date: Sun, 14 Apr 2019 19:03:02 GMT
server: Cowboy
x-request-id: FZVsiSTULGUhyEwAAA3E

{
  "meta": {
    "status": "Unauthenticated"
  }
}

Gördüğünüz gibi :ensure_auth pipeline'ı yetkisiz erişimi engelledi. Önce gidip bir access token almamız gerekiyor. Bunu da aşağıdaki gibi deneyelim.

➜ http --form POST :4000/api/auth/sign_in 'user[email]=muratbsts@gmail.com' 'user[password]=password'
HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 467
content-type: application/json; charset=utf-8
date: Sun, 14 Apr 2019 19:13:31 GMT
server: Cowboy
x-request-id: FZVtG8GKfJ2sj3oAABIh

{
    "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.....JWRohW7yVxs978V4SV87g2BUFBT1BlpYNtsSfiOxPvss1hOiuUyZ1ZqIYOwyJ0-DYSPs63oEPGdA..."
}

Ve bu token ile users endpointimize request yaparsak aşağıdaki gibi bir başarılı bir sonuç alacağız.

http :4000/api/v1/users 'Authorization:Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.....JWRohW7yVxs978V4SV87g2BUFBT1BlpYNtsSfiOxPvss1hOiuUyZ1ZqIYOwyJ0-DYSPs63oEPGdA...'
HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 1536
content-type: application/json; charset=utf-8
date: Sun, 14 Apr 2019 19:15:32 GMT
server: Cowboy
x-request-id: FZVtN9wwpCWV8KcAABJh

{
  "data": [
    {
      "email": "muratbsts@gmail.com",
      "id": 1,
      "inserted_at": "2019-04-03T21:16:09",
      "name": "murat",
      "updated_at": "2019-04-03T21:16:09"
    },
    ....
  }
]

Ve böylece ikinci bölümün de sonuna geldik. Bir sonraki bölümde kaydolan kullanıcılara email göndererek doğrulama yapacağız...

Not: Bu yazıyı beğendiyseniz medium profilimden de yerebilir ve ya övebilirsiniz...

Görüşler

1
eviduslu

agzina saglik, koluna, gonlune ve yuregine saglik kardesim. Hayirlara vesile olmasi dilegiyle

1
Zakkum

Eline saglik!

1
murat

Teşekkürler, faydalı olmuştur dileği ile..

Görüş belirtmek için giriş yapın...

İlgili Yazılar

Fazlamesai'ye soralım: 2017'de ne öğrenmeli?

butch

2017 kapıya dayandı. Türkiye için ekonomik açıdan sarsıntılı bir yılı geride bırakmak üzereyiz. Kafalar çok karışık, çoğumuz nasıl hareket etmemiz gerektiğini sorguluyoruz. Kimimiz var olan düzenini korumaya çalışıyor, kimimiz girişim yapmak için en uygun zamanda olduğunu düşünüyor.

Bugün Hacker News'de gördüğüm "2017'de ne öğrenmeyi düşünüyorsunuz?" başlığının burada da yer...

Eski Markalar, Yeni Nesil Eğitim Araçları

butch

Bootstrap - Üniversite Öncesi Bilgisayar Bilimleri Eğitimi yazısını okuduktan sonra aklıma bu konu takıldı. Yazılım temelli bir eğitim modelinin hayatın her aşamasında faydalı olacağı düşüncesine katılmamak mümkün değil ama bunu farklı yöntemlerle (robotik, elektronik ...) geliştirmek, çeşitlilik sunarak her çocuğun dünyasına girebilmek - özellikle maker kültürü ilkokul düzeyine kadar inmişken...

Google Görüşme Üniversitesi: Web Geliştiricisinin Yazılım Mühendisliğine Yolculuğu

butch

John Washam, eğitimini ekonomi alanında tamamlamış ama kariyerini web programcısı olarak yapmış biri. Başarılı olan bir kaç iş de kurmuş. Hayatını bu alandan kazanıyor. Kariyerini, çocukluğunda hayal ettiği yazılım geliştiricilik üzerine kurduğunu, iyi bir yazılımcı olduğunu düşünürken bunun doğru olmadığı gerçeğiyle yüzleşmiş. Bundan birkaç yıl önce iş aramaya karar verdiğinde, aslında...

Geleneksel üniversiteye mecbur muyuz?

butch

Özellikle ülkemizde, sıkça kulak misafiri olduğumuz konulardır, "Kendime uygun yüksek lisans/doktora programı bulamıyorum", "GPA'm çok düşük beni o programa kabul etmezler", "Yüksek lisansla birlikte götüremiyorum, işi bıraktım" ve türevleri. Acaba bu durumu gerçekten sorguluyor muyuz yoksa hala eski alışkanlıkların kurbanı olmaya devam mı ediyoruz.

Günümüzde...

Fazlamesai'ye soralım: 2018'de ne öğrenmeli?

murat

2018 de geldi, geçiyor. @butch'un "Fazlamesai'ye soralım: 2017'de ne öğrenmeli?" yazısını görünce bu konuyu 2018 için de hortlatmak gerektiğini düşündüm.

Ben öğrenecek yeni bir programlama dili arayışındayım. Şuan için seçeneklerim şu şekilde: python, go, elixir ve clojure.

Sizce 2018'de ne öğrenmeli?