Scala用GraphQLフレームワークのsangriaでUpdateCtx
を使って認証処理を実装する。
使い方に注意点がいくつかあるが、まずは普通に動かすための方法について。
UpdateCtxを使って実装する
認証処理に関するドキュメントはLearn SangriaのAuthentication and Authorisationセクションにあり、「Resolve-Based Auth」として紹介されているようにUpdateCtx
を使って実装してみる。
その前に、まずはsangriaを動かすための前提となるCtx
および共通して使用する型を定義する。
色々実装が雑なのはサンプルのためなので目を瞑る。
コード全体像はGithubにpushしてある。
// ユーザー
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を実装する。
// 入力
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を用意する。
// 入力
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として公開する。
val mutation = ObjectType(
"Mutation",
fields[GraphQLContext, Unit](
// loginするのとuserのget/updateをMutationとして公開
authenticateField ++ userField : _*
)
)
このmutation
を使ってSchema
を作ってGraphQLのQueryを実行してみる
mutation Login {
login(email: "hoge@example.com", password: "password") {
value
}
get {
id
name
}
update(name: "updated!") {
id
name
}
}
結果は以下のように得ることが出来る。
{
"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
と上から順に実行されている。
なので、順番を入れ替えるとダメになる。
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
は無事に成功する。
{
"data": {
"get": null,
"login": {
"value": "1"
},
"update": {
"id": 1,
"name": "updated"
}
}
}
つまりUpdateCtx
はそれ以降のCtx
を更新するものなので、使用する場合はクライアント側で順番に注意して記述する必要がある。
UpdateCtxはqueryでは効かない
上ではmutationのサンプルを実装したが、queryでも試してみる。
login中のuser情報を返すだけのqueryを実装する。
val userQuery = ObjectType(
"Query",
fields[GraphQLContext, Unit](
Field(
"get", // mutationのgetと同じ
OptionType(userType),
arguments = Nil,
resolve = ctx => ctx.ctx.userOpt
)
)
)
これでSchemaを作成し、実行してみる。
投げるクエリは以下。
先程のmutation LoginAndUpdate
からquery
に変化しているだけ。
query MyQuery {
login(email: "user-1@example.com", password: "password") {
value
}
get {
id
name
}
}
実行してみると以下の結果が得られる。
{
"data": {
"login": {
"value": "token-1"
},
"get": null
}
}
つまり、queryではUpdateCtx
が効いていない。
UpdateCtx
はmutationでしか有効でないので、queryでも認証したければUpdateCtx
以外で行う必要がある。
ネストしたパスではUpdateCtxが効かない
GraphQLだとトップレベルにクエリを並べることが多いとは思うが、入れ子にすることも出来る。
先程実装したmutationにprefix
を付けてみる。
val mutation =
ObjectType(
"Mutation",
fields[GraphQLContext, Unit](
Field("prefix",
ObjectType("prefix",
fields[GraphQLContext, Unit](authenticateFields ++ userMutation: _*)),
resolve = _ => ()
)
)
)
これに対して先程と同じようにlogin
してget
してupdate
するクエリを投げてみる。
prefix
というパスの中に全部入る形となる。
mutation LoginAndUpdate {
prefix {
login(email: "user-1@example.com", password: "password") {
value
}
get {
id
name
}
update(name: "updated") {
id
name
}
}
}
結果はこんな感じ。
{
"data": null,
"errors": [
{
"message": "Internal server error",
"path": [
"prefix",
"update"
],
"locations": [
{
"line": 41,
"column": 5
}
]
}
]
}
update
の内部でOption.get
しているところでNoSuchElementException
がthrowされてしまっている。
prefix > login
だけにしてみると、期待通り実行されている。
mutation Login {
prefix {
login(email: "user-1@example.com", password: "password") {
value
}
}
}
結果は以下の通り。
{
"data": {
"prefix": {
"login": {
"value": "token-1"
}
}
}
}
UpdateCtx
を使うFieldはトップレベルに配置すること。
つまり、先ほどprefix
に入れ込んだ実装を少し変えてauthenticateFields
だけ外に出すと期待通り動くようになる。
val mutation =
ObjectType("Mutation",
// ここでauthenticateFieldsはトップレベルに配置する
authenticateFields ++ fields[GraphQLContext, Unit](
Field("prefix", // それ以外はネストしていても問題ない
ObjectType("prefix", fields[GraphQLContext, Unit](userMutation: _*)),
resolve = _ => ())
))
これに対してlogin
してget
してupdate
するクエリを投げてみる。
mutation LoginAndUpdate {
login(email: "user-1@example.com", password: "password") {
value
}
prefix {
get {
id
name
}
update(name: "updated") {
id
name
}
}
}
結果は期待通り、無事にUpdateCtx
が機能している。
{
"data": {
"login": {
"value": "token-1"
},
"prefix": {
"get": {
"id": 1,
"name": "user-1"
},
"update": {
"id": 1,
"name": "updated"
}
}
}
}
所感
UpdateCtx
の使い方には癖があるが、GraphQLのクエリだけで認証を完結させられるので便利。
しかし、Queryで利用できないのは厳しい。
実際のプロダクト開発だとHttp-Headerに認証Tokenを入れてGraphQLとは別のレイヤーで認証することになりそう。