LoginSignup
15
6

More than 5 years have passed since last update.

GraphQL for Elixir#3 Middlewareと認証について考える

Last updated at Posted at 2018-07-07

(この記事は「fukuoka.ex x ザキ研 Advent Calendar 2017」の20日目です)

昨日は @takasehideki さんの「ElixirでIoT#1.3.3:logistic_mapをIoTボードで性能評価してみた」でした。

はじめに

|> GraphQL for Elixir#1 基本的な実装について考える
|> GraphQL for Elixir#2 リクエストとレスポンスの値について考える

今まで基本的な実装を進めて来ました。次はもう少し複雑なmiddlewareと認証に付いて実装を進めて行きます。

ミドルウェア

Absintheでは通常ではschema のresolveで呼ばれる関数でレスポンスの値を返します。ただここだけで処理をした場合、resolveないで実行される関数が肥大化したり、全てのリクエストで処理したいものを全てのresolveに追加しないといけません。
middlewareではschemaないで定義することでresolveが呼ばれる前に実行したり、resolveを実行した後に処理を挟み込むことができます。

Absinthe.Middleware

Absinthe.Middlewareをbehaviourで使います。これでresolveの実行前と実行後に処理を挟むことができます。

defmodule MyApp.Authentication do
  @behaviour Absinthe.Middleware

  def call(resolution, _config) do
    case resolution.context do
      %{current_user: _} ->
        resolution

      _ ->
        resolution
        |> Absinthe.Resolution.put_result({:error, message: "unauthenticated"})
    end
  end
end

上記内容は context 内に current_userがあるかどうかを判定し、ある場合はそのままresolutionを返し、ない場合はエラーを追加して返信します。

query do
  field :all_todos, list_of(:todo) do
    middleware(PolarisWeb.Authentication)
    resolve(&Polaris.Resolver.TodoContent.all/2)
  end
end

これを実行するためにfieldないで Absinthe.Schema.Notation.middlewareを利用します。
これでall_todosのschemaが呼ばれた時に、MyApp.Authentication モジュールの callが呼ばれます。

Middlewareを使っての認証

ここで先ほどのmiddlewareを使ってGraphQLで認証機能を実装しようと思います。
まずは先ほどのMyApp.Authenticationモジュールのcall関数内でresolution.contextにcurrent_userがあるかどうか判定を行なっていました。このcurrent_userがある場合は認証が通ってると判定し、ない場合は認証失敗の通知を送ります。
ではこのcurrent_userがどのタイミングでresolution.contextにcurernt_userが入るか確認してみたいと思います。

Phoenix Plug

まずはGraphQLのschemaが呼ばれる前に実行するものとしてPhoenixのPlugないでcurrent_userを入れるようにしたいと思います。

router.ex
pipeline :api do
  plug(:accepts, ["json"])
  plug(MyApp.Auth, repo: MyApp.Repo)
end

ここでMyApp.Auth をPlugとして読み込みます。

auth.ex
def call(conn, _) do
  with %{current_user: user} <- build_context(conn) do
    conn
    |> put_private(:absinthe, %{context: %{current_user: user}})
    |> assign(:current_user, user)
  else
    _ ->
      conn
  end
end

Plugではcall関数が呼ばれます。まずはこのなかでbuild_context関数を呼び、返り値にcurrent_userが返って来た場合にそれをAbsintheのMiddlewareで参照できるよう、connに値を追加します。

auth.ex
def build_context(conn) do
  with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
       {:ok, current_user} <- authorize(token) do
    %{current_user: current_user}
  else
    _ -> :error
  end
end

まずは build_context関数でheaderからtokenを取得します。tokenはRFC6750のフォーマットにそってauthorizationヘッダーにBearer トークンでヘッダーに追加しています。

auth.ex
defp authorize(authorization) do
  with {:ok, sub} <- check_authentication(authorization) do
    user = Repo.get_by(User, id: sub)
    {:ok, user}
  else
    _ ->
      {:error, "invalid authorization token"}
  end
end

authorize関数内でtokenのチェックを行います。返り値からuserをDBより取得し、それを返り値とします。ここでtokenが不正の場合はエラーを返します。

auth.ex
defp check_authentication(token) do
  with {:ok, c} <- Polaris.Guardian.decode_and_verify(token),
       %{"exp" => exp, "nbf" => nbf, "sub" => sub, "appkey" => appkey} <- c,
       appkey <- Application.fetch_env!(:settings, :key),
       now <- Timex.Timezone.convert(Timex.now(), "Asia/Tokyo"),
       exp <- Timex.Timezone.convert(Timex.from_unix(exp), "Asia/Tokyo"),
       nbf <- Timex.Timezone.convert(Timex.from_unix(nbf), "Asia/Tokyo"),
       Timex.between?(now, nbf, exp) do
    {:ok, sub}
  else
    _ ->
      {:error, "invalid authorization token"}
  end
end

check_authentication関数でtokenの内容を確認します。tokenはJWTを利用していますので、まずはtokenをdecodeします。

deocodeした結果exp、nbf、sub、appkeyを取得します。JWTがこのアプリケーションから発行されたものかどうか判定するためappkeyが正しいか判定します。
次に、現在の日付が、tokenの有効期限ないかどうか判定します。有効期限ないだった場合はこのtokenを作った時のもととなるuser idがsubに入っているため、subを返します。

これでGraphQLでtokenを利用しての認証となります。

まとめ

GraphQLではmiddlewareとPlugを利用し処理を挟み込み、認証を行うことができます。
応用することでエラーハンドリング、データ整形等を行うことができます。

明日の「fukuoka.ex x ザキ研 Advent Calendar 2017」の記事は, @piacere_ex さんの
関数型でデータサイエンス#4:インプットしたデータを集約する②
です.お楽しみに!

15
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
6