📑
KtorでFirebaseAuthを実装する
タイトルのままですが、Ktorというフレームワークで認証バックエンドにFirebaseを利用できるようにするお話です。
Ktor、ある程度の認証機構は揃ってるからそのままでも使いやすいよね。
でもFirebase Authはデフォルトでないんだよね
説明角のめんどくさいからコード内のコメントでも読んでくれ
環境
この記事を執筆したタイミングでの環境はこんな感じ
build.gradle.kts
implementation("com.google.firebase:firebase-admin:9.0.0")
var ktor_version = "2.1.0"
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("io.ktor:ktor-server-resources:$ktor_version")
implementation("io.ktor:ktor-server-auth:$ktor_version")
実装
認証機構本体の実装
FirebaseAuthProvider.kt
class FirebaseAuthProvider(configuration: Configuration) {
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, FirebaseAuthProvider> {
override val key = AttributeKey<FirebaseAuthProvider>("FirebaseAuthProvider")
// デコードされたトークンを取りだすためのキー
internal val DecodedTokenKey: AttributeKey<FirebaseToken> = AttributeKey("FirebaseAuthenticationDecodedTokenKey")
/**
* モジュールのインストールをする
*/
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): FirebaseAuthProvider {
val plugin = FirebaseAuthProvider(Configuration().apply(configure))
// 適切なフェーズがわからんのでとりあえずモニタリングにしてるよ
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
plugin.intercept(this)
}
return plugin
}
}
private val auth = FirebaseAuth.getInstance(configuration.firebaseApp ?: FirebaseApp.getInstance())
private val authorizationType = configuration.authorizationType
private val checkRevoked = configuration.checkRevoked
private val skipWhen = configuration.skipWhen
private val whenReject = configuration.whenReject
class Configuration {
// Firebaseインスタンス
var firebaseApp: FirebaseApp? = null
// 認証ヘッダ
var authorizationType = AuthorizationType.Jwt
// JWTに対してユーザが有効かどうかまで確認する?
// false: 確認しない
// true: 確認する
var checkRevoked = false
// 条件に当てはまるときに認証をスキップさせる
var skipWhen: (suspend (ApplicationCall) -> Boolean)? = null
// 認証処理中に例外が発生したらそのハンドルをさせる
var whenReject: (suspend (ApplicationCall, Throwable) -> Unit)? = null
}
sealed class AuthorizationType {
abstract val getIdToken: (ApplicationCall) -> String?
object Bearer : AuthorizationType() {
override val getIdToken: (ApplicationCall) -> String? = {
it.request.headers.get(HttpHeaders.Authorization)?.trim()?.split(" ")?.let { authorization ->
val (phrase, token) = authorization.let { it.getOrNull(0) to it.getOrNull(1) }
phrase?.lowercase()?.equals("bearer")?.takeIf { it }?.let { token?.trim() }
}
}
}
object Jwt : AuthorizationType() {
override val getIdToken: (ApplicationCall) -> String? = {
it.request.headers.get(HttpHeaders.Authorization)?.trim()?.split(" ")?.let { authorization ->
val (phrase, token) = authorization.let { it.getOrNull(0) to it.getOrNull(1) }
phrase?.lowercase()?.equals("jwt")?.takeIf { it }?.let { token?.trim() }
}
}
}
}
private suspend fun intercept(context: PipelineContext<Unit, ApplicationCall>) {
with(context.call) {
if (skipWhen?.invoke(this) == true) {
return@with
}
kotlin.runCatching {
val idToken = authorizationType.getIdToken(this)
if (idToken.isNullOrBlank()) {
throw TokenNotProvidedException()
}
val decodedToken = auth.verifyIdToken(idToken, checkRevoked)
attributes.put(DecodedTokenKey, decodedToken)
}.onFailure {
whenReject?.invoke(this, it)
}
}
}
internal class InvalidTokenException : Exception()
internal class TokenNotProvidedException : Exception()
}
fun ApplicationCall.getDecodedToken(): FirebaseToken? = attributes.getOrNull(DecodedTokenKey).also { Result }
アプリへの組み込み
KtorApplication.kt
install(FirebaseAuthProvider) {
firebaseApp = FirebaseApp.getInstance()
authorizationType = FirebaseAuthProvider.AuthorizationType.Jwt
checkRevoked = true
skipWhen = { call ->
// 例えば mock=1 がクエリパラメータに入っていたら認証をスキップしたり.....
call.request.queryParameters.get("mock")?.toIntOrNull()?.equals(1)
}
whenReject = { call, cause ->
when (cause) {
// 例外ごとに対処したり、call.responseしたり.....
is FirebaseAuthProvider.TokenNotProvidedException -> {
// .....
}
is FirebaseAuthException ->{
//
}
is IllegalArgumentException ->{
// ......
}
else ->{
// ......
}
}
}
}
検証結果の取り出し
routing{
get("/"){
val firebaseUser = call.getDecodedToken()
// do something.....
}
}
クライアントからの呼び出し
今回はJWTキーを指定したとして、そうするとこんな感じで認証ヘッダを詰める
curl --header "authorization: JWT xxxxxxxxxxxx"
Discussion