[Scala]SangriaでUpdateCtxを使ってGraphQLの認証を実装する

Scala用GraphQLフレームワークの[sangria](https://github.com/sangria-graphql/sangria/)で`UpdateCtx`を使って認証処理を実装する。
使い方に注意点がいくつかあるが、まずは普通に動かすための方法について。

## UpdateCtxを使って実装する

認証処理に関するドキュメントは[Learn Sangria](http://sangria-graphql.org/learn)の[Authentication and Authorisation](http://sangria-graphql.org/learn/#authentication-and-authorisation)セクションにあり、「Resolve-Based Auth」として紹介されているように[`UpdateCtx`](https://github.com/sangria-graphql/sangria/blob/37a9b5bf9ae267da9c8e0e34226ed60f2d676203/src/main/scala/sangria/schema/Context.scala#L129-L131)を使って実装してみる。

その前に、まずはsangriaを動かすための前提となる`Ctx`および共通して使用する型を定義する。
色々実装が雑なのはサンプルのためなので目を瞑る。
コード全体像は[Github](https://github.com/petitviolet/scala-graphql-prac/blob/e3f7ddeebfd02325ab5b3c4de37fbc33d0f18e7c/modules/main/src/main/scala/net/petitviolet/prac/graphql/sample/sample.scala#L208-L343)にpushしてある。

```scala
// ユーザー
case class User(id: Long, name: String, email: String, password: String) {
  def updateName(newName: String) = copy(name = newName)
}
trait UserDao {  // databaseへの接続.実装は省略
    def findAll: Seq[User]
    def login(email: String, password: String): Try[Token]
    def findByToken(token: Token): Option[User]
    def update(newUser: User): Unit
}
// 認証トークン
case class Token(value: String)

// Ctxとして使用
case class GraphQLContext(userOpt: Option[User] = None) {
  val userDao: UserDao = ???
  // ログイン処理してtoken発行
  def loggedIn(user: User): GraphQLContext = copy(userOpt = Some(user))
  // tokenからuserを見付けて認証
  def authenticate(token: Token): GraphQLContext = copy(userOpt = userDao.findByToken(token))
}
```

`GraphQLContext.userDao.login`を使って認証するMutationを実装していく。

まずはloginして`Ctx`を`UpdateCtx`で更新するFieldを実装する。

```scala
// 入力
val emailArg = Argument("email", StringType, "email of user")
val passwordArg = Argument("password", StringType, "password of user")

// 出力
val authenticateType: ObjectType[Unit, Token] = derive.deriveObjectType[Unit, Token]()

// loginしてUpdateCtxするField
val authenticateField = fields[GraphQLContext, Unit](
  Field(
    "login",
    authenticateType,
    arguments = emailArg :: passwordArg :: Nil,
    resolve = { ctx =>
      ctx.withArgs(emailArg, passwordArg) { (email, password) =>
        // login処理を実行
        UpdateCtx(ctx.ctx.userDao.login(email, password)) { token: Token =>
          // loginが成功(Successなら)したらctxを更新
          ctx.ctx.authenticate(token)
        }
      }
    }
  )
)
```

続いてログイン中のUserの情報を取得する、Userを更新するFieldを用意する。

```scala
// 入力
val nameArg = Argument("name", StringType, "name of user")
// 出力
val userType: ObjectType[Unit, User] = derive.deriveObjectType[Unit, User]()

val userField = fields[GraphQLContext, Unit](
  Field(
    "get", // login中のuserを取得する
    OptionType(userType),
    arguments = Nil,
    resolve = ctx => ctx.ctx.userOpt
  ),
  Field(
    "update",  // nameを更新する
    userType,
    arguments = nameArg :: Nil,
    resolve = { ctx =>     
      ctx.withArgs(nameArg) { name =>
        val user = ctx.ctx.userOpt.get // 雑にOption.get
        val newUser = user.updateName(name)
        UserDao.update(newUser)
        newUser
      }
    }
  )
)
```

これらのFieldをMutationとして公開する。

```scala
val mutation = ObjectType(
  "Mutation", 
  fields[GraphQLContext, Unit](
    // loginするのとuserのget/updateをMutationとして公開
    authenticateField ++ userField : _*
  )
)
```

この`mutation`を使って`Schema`を作ってGraphQLのQueryを実行してみる

```graphql
mutation Login {
  login(email: "hoge@example.com", password: "password") {
    value
  }
  get {
    id 
    name
  }
  update(name: "updated!") {
    id 
    name
  }
}
```

結果は以下のように得ることが出来る。

```json
{
  "data": {
    "login": {
      "value": "token-1"
    },
    "get": {
      "id": 1,
      "name": "user-1"
    },
    "update": {
      "id": 1,
      "name": "updated"
    }
  }
}
```

無事に`login`して、`UpdateCtx`によって`Ctx`に与えられたUserの情報を`get`や`update`で参照することが出来た。

## UpdateCtxの注意点

便利な`UpdateCtx`だが注意点がいくつかある。

### クエリ内でUpdateCtxを記述する位置

`UpdateCtx`があるFieldを考慮して実行順序をいい感じにしてくれるようにはなっていない。 
単純に書いた順番、先程の例だと`login`, `get`, `update`と上から順に実行されている。  
なので、順番を入れ替えるとダメになる。

```graphql
mutation LoginAndUpdate {
  get {
    id 
    name
  }
  login(email: "user-1@example.com", password: "password") {
    value 
  }  
  update(name: "updated") {
    id
    name 
  }
}
```

結果は`get`だけlogin中のuserが`Ctx`から取得できない。  
`login`して`update`は無事に成功する。

```json
{
  "data": {
    "get": null,
    "login": {
      "value": "1"
    },
    "update": {
      "id": 1,
      "name": "updated"
    }
  }
}
```

つまり`UpdateCtx`はそれ以降の`Ctx`を更新するものなので、使用する場合はクライアント側で順番に注意して記述する必要がある。

### UpdateCtxはqueryでは効かない

上ではmutationのサンプルを実装したが、queryでも試してみる。
login中のuser情報を返すだけのqueryを実装する。

```scala
val userQuery = ObjectType(
  "Query", 
  fields[GraphQLContext, Unit](
    Field(
      "get", // mutationのgetと同じ
      OptionType(userType),
      arguments = Nil,
      resolve = ctx => ctx.ctx.userOpt
    )
  )
)
```

これでSchemaを作成し、実行してみる。  
投げるクエリは以下。
先程の`mutation LoginAndUpdate`から`query`に変化しているだけ。

```graphql
query MyQuery {
  login(email: "user-1@example.com", password: "password") {
    value 
  }   
  get {
    id 
    name 
  }
} 
```

実行してみると以下の結果が得られる。

```json
{
  "data": {
    "login": {
      "value": "token-1"
    },
    "get": null
  }
}
```

つまり、queryでは`UpdateCtx`が効いていない。
`UpdateCtx`はmutationでしか有効でないので、queryでも認証したければ`UpdateCtx`以外で行う必要がある。

### ネストしたパスではUpdateCtxが効かない

GraphQLだとトップレベルにクエリを並べることが多いとは思うが、入れ子にすることも出来る。
先程実装したmutationに`prefix`を付けてみる。

```scala
val mutation =
  ObjectType(
    "Mutation",
    fields[GraphQLContext, Unit](
      Field("prefix",
        ObjectType("prefix",
          fields[GraphQLContext, Unit](authenticateFields ++ userMutation: _*)),
          resolve = _ => ()
        )
      )
    )
```

これに対して先程と同じように`login`して`get`して`update`するクエリを投げてみる。
`prefix`というパスの中に全部入る形となる。

```graphql
mutation LoginAndUpdate {
  prefix {
    login(email: "user-1@example.com", password: "password") {
      value 
    }  
    
    get {
      id 
      name
    }

    update(name: "updated") {
      id
      name 
    } 
  }
}
```

結果はこんな感じ。  

```json
{
  "data": null,
  "errors": [
    {
      "message": "Internal server error",
      "path": [
        "prefix",
        "update"
      ],
      "locations": [
        {
          "line": 41,
          "column": 5
        }
      ]
    }
  ]
}
```

`update`の内部で`Option.get`しているところで`NoSuchElementException`がthrowされてしまっている。

`prefix > login`だけにしてみると、期待通り実行されている。

```graphql
mutation Login {
  prefix {
    login(email: "user-1@example.com", password: "password") {
      value 
    }  
  }
}
```

結果は以下の通り。

```json
{
  "data": {
    "prefix": {
      "login": {
        "value": "token-1"
      }
    }
  }
}
```

`UpdateCtx`を使うFieldはトップレベルに配置すること。
つまり、先ほど`prefix`に入れ込んだ実装を少し変えて`authenticateFields`だけ外に出すと期待通り動くようになる。

```scala
val mutation =
  ObjectType("Mutation",
    // ここでauthenticateFieldsはトップレベルに配置する
    authenticateFields ++ fields[GraphQLContext, Unit](
      Field("prefix", // それ以外はネストしていても問題ない
        ObjectType("prefix", fields[GraphQLContext, Unit](userMutation: _*)),
        resolve = _ => ())
    ))
```

これに対して`login`して`get`して`update`するクエリを投げてみる。

```graphql
mutation LoginAndUpdate {
  login(email: "user-1@example.com", password: "password") {
    value
  }
  prefix {
    get {
      id
      name
    }
    update(name: "updated") {
      id
      name
    }
  }
}
```

結果は期待通り、無事に`UpdateCtx`が機能している。

```json
{
  "data": {
    "login": {
      "value": "token-1"
    },
    "prefix": {
      "get": {
        "id": 1,
        "name": "user-1"
      },
      "update": {
        "id": 1,
        "name": "updated"
      }
    }
  }
}
```

## 所感

`UpdateCtx`の使い方には癖があるが、GraphQLのクエリだけで認証を完結させられるので便利。
しかし、Queryで利用できないのは厳しい。
実際のプロダクト開発だとHttp-Headerに認証Tokenを入れてGraphQLとは別のレイヤーで認証することになりそう。