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

（この記事は「[fukuoka.ex x ザキ研 Advent Calendar 2017](https://adventar.org/calendars/2873)」の20日目です)

昨日は @takasehideki さんの「[ElixirでIoT#1.3.3：logistic_mapをIoTボードで性能評価してみた](https://qiita.com/takasehideki/items/4c771b8b7db18c3be0fc)」でした。

# はじめに
|> [GraphQL for Elixir#1 基本的な実装について考える](https://qiita.com/kobatako/items/df18a99b8601c9a426ad)
|> [GraphQL for Elixir#2 リクエストとレスポンスの値について考える](https://qiita.com/kobatako/items/cf34dc3e97534976a50d)

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

# ミドルウェア

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

## Absinthe.Middleware

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

```Elixir
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`を返し、ない場合はエラーを追加して返信します。

```Elixir
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を入れるようにしたいと思います。

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

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

```Elixir: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に値を追加します。

```Elixir: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](https://tools.ietf.org/html/rfc6750)のフォーマットにそってauthorizationヘッダーに`Bearer トークン`でヘッダーに追加しています。

```Elixir: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が不正の場合はエラーを返します。

```Elixir: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](https://adventar.org/calendars/2873)」の記事は， @piacere_ex さんの
「[関数型でデータサイエンス#4：インプットしたデータを集約する②](https://qiita.com/piacere_ex/items/7c33836f930b63b0444d)」
です．お楽しみに！