Scala - SangriaでUpdateCtxを使ってGraphQLの認証を実装する
2018-04-27
QiitaScalaGraphQLsangriaScala 用 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 とは別のレイヤーで認証することになりそう。
from: https://qiita.com/petitviolet/items/1fb6a8e52f02f4309f5b