Open6

KotlinのAPIサーバー開発の自分的標準構成模索 Ktor, Graphql Kotlin, Exposed, Koin, Cognito

snicmakinosnicmakino

JWTとGraphQL Kotlinの連携

GraphQLの各クエリーで、JWTのユーザーを取得する方法
Ktor:2.3.7
graphql-kotlin-ktor-server:7.0.2

JWTの設定

import com.auth0.jwk.JwkProviderBuilder
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*

fun Application.configureJwt() = environment.config.run {
    install(Authentication) {
        jwt {
            val audience = property("jwt.audience").getString()
            val issuer = property("jwt.issuer").getString()
            val jwkProvider = JwkProviderBuilder(issuer).build()

            verifier(jwkProvider, issuer)
            validate { credential ->
                JWTPrincipal(credential.payload)
            }
        }
    }
}

上の設定でcall.principalにJWTPrincipalが設定される。
(Principalは任意のカスタマイズしたものでもOK)

GraphQL設定

fun Application.graphQlModule() {
    install(GraphQL) {
        schema {
            packages = listOf("presentation.graphql")
            queries = listOf(
                HelloWorldQuery()
            )
            schemaObject = AppSchema()
        }
        server {
            contextFactory = CustomGraphQLContextFactory()
        }
    }
    routing {
        authenticate(optional = true) {
            graphQLPostRoute()
        }
    }
}

class CustomGraphQLContextFactory : DefaultKtorGraphQLContextFactory() {
    override suspend fun generateContext(request: ApplicationRequest): GraphQLContext {
        val principal = request.call.principal<JWTPrincipal>()
            ?: return super.generateContext(request)

        return super.generateContext(request).plus(
            mapOf("principal" to principal)
        )
    }
}

class HelloWorldQuery : Query {
    fun hello(dfe: DataFetchingEnvironment): String {
        val principal = dfe.graphQlContext.get<JWTPrincipal?>("principal") ?: return "Hello World!"
        return "Hello ${principal["username"]}!"
    }
}

CustomGraphQLContextFactoryで、セットされたApplicationCall内のPrincipalを取得し、GraphQL Contextに登録する。
GraphQL Contextは各クエリ等で利用できるので、JWTの情報を扱うことができる。

※ authenticate(optional = true)を使うと、認証がオプショナルになるので、認証されていなくても各ルーティングが行われるようになる。

snicmakinosnicmakino

DIを設定する

class HelloWorldQuery(service: HelloWorldService) : Query {
    fun hello(dfe: DataFetchingEnvironment): String {
        val principal = dfe.graphQlContext.get<JWTPrincipal?>("principal") ?: return "Hello World!"
        val hello = service.getHello(principal["username"])
        return hello
    }
}

interface HelloWorldService {
    fun getHello(name: String): String
}

class HelloWorldServiceImpl : HelloWorldService {
    override fun getHello(name: String): String {
        return "Hello ${name}!"
    }
}

Koin.kt


fun Application.configureDi() {
    install(Koin) {
        modules(appModules())
    }
}

fun appModules() = listOf(module {
    single<HelloWorldService> { HelloWorldServiceImpl() }
    singleOf(::HelloWorldQuery)
})

Application.kt

fun Application.graphQlModule() {
    val helloWorldQuery by inject<HelloWorldQuery>()
    install(GraphQL) {
        schema {
            packages = listOf("presentation.graphql")
            queries = listOf(
                helloWorldQuery,
            )
            schemaObject = AppSchema()
        }
        server {
            contextFactory = CustomGraphQLContextFactory()
        }
    }
    routing {
        authenticate(optional = true) {
            graphQLPostRoute()
        }
    }
}

KoinでInjectしたインスタンスを、graphQlModuleのqaueriesやmutationsに渡すとうまくいく。

snicmakinosnicmakino

Exposed has two flavors of database access: typesafe SQL wrapping DSL and lightweight Data Access Objects (DAO)

ExposedはSQLをラッピングしたDSLと、DAOと、2種類のDBアクセスを提供している。

第一印象、検索のような複雑なクエリはDSLを使って、更新や登録はDAO使うのが良さそうかな。例外はあると思うけど。

snicmakinosnicmakino

DAOでも割と柔軟に検索できそうだからいったんDAOでできるところまでやるのが良さそう