(この記事は「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を入れるようにしたいと思います。
pipeline :api do
plug(:accepts, ["json"])
plug(MyApp.Auth, repo: MyApp.Repo)
end
ここでMyApp.Auth
をPlugとして読み込みます。
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に値を追加します。
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 トークン
でヘッダーに追加しています。
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が不正の場合はエラーを返します。
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:インプットしたデータを集約する②」
です.お楽しみに!