KtorでMock Authentication(モック認証)

# 認証を モック化したい ことがある
**Ktor**アプリケーションのRoute上のAPIをテストしたい場合、ログイン等の認証が必須のものについては、何らかの方法で認証を突破する必要があります。
認証方式によって突破方法はさまざまですが、今回はKtorの**Authentication**プラグイン(Feature)を**拡張実装**するアプローチをご紹介します。

代表的な恩恵は、HTTPリクエストヘッダーのAuthorization属性にモックの情報の差し込む、などの小細工が必要なくなることでしょうか。さっそく見ていきましょう。

## 1. モック認証用の拡張関数の作成
Ktorの`Authentication`プラグインにデフォルトで実装されている*[form](https://github.com/ktorio/ktor/blob/main/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/FormAuth.kt)*や*[basic](https://github.com/ktorio/ktor/blob/main/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/BasicAuth.kt)*といった関数を参考に、**セットした認証主体の情報を返すだけの拡張関数**を作成します。
実装すべきは大きく**2つ**、①`Authentication.Configuration`の拡張関数と、②拡張関数中で利用する`AuthenticationProvider`の継承クラスです。

```kotlin
// io.ktor.auth.Authenticationを拡張
fun Authentication.Configuration.mock(
    name: String? = null,
    configure: MockAuthenticationProvider.Configuration.() -> Unit
) {
    val provider = MockAuthenticationProvider.Configuration(name).apply(configure).build()

    // 認証時に実行される処理の定義
    provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
        // 認証主体の取得関数を実行(関数は別のところで設定)
        val principal = provider.principalProvider(call)

        // 認証主体がnullなら401を返して認証を中断
        if (principal == null) {
            call.respond(UnauthorizedResponse())
            context.challenge.complete()
            return@intercept
        }

        // (認証情報がnullでない場合) 
        // 認証主体をcontextにセット -> call.principalで取得可能に
        context.principal(principal)
    }
    register(provider)
}
```
```kotlin
// io.ktor.auth.AuthenticationProviderを継承
class MockAuthenticationProvider(config: Configuration) : AuthenticationProvider(config) {
    val principalProvider = config.principalProvider

    // io.ktor.auth.AuthenticationProvider.Configurationを継承
    class Configuration(name: String?) : AuthenticationProvider.Configuration(name) {
        var principalProvider: ApplicationCall.() -> Principal? = { null }
        fun build() = MockAuthenticationProvider(this)

        // 「認証主体の情報をセットする関数」を渡す関数
        fun principal(principalProvider: ApplicationCall.() -> Principal?) {
            this.principalProvider = principalProvider
        }
    }
}
```


## 2. モック認証のinstallerの実装
認証プラグインをinstallする拡張関数を実装します。
ここで`principal{ }`関数に渡した情報が、ルーティング上の*call.principal()*から取得されます。
利用ごとに認証情報を切り替えたい場合は、関数の引数などから情報を差し込めるようにしておきます。以下、サンプルです。

```kotlin
// io.ktor.application.Application
fun Application.testAuth(loginUser: LoginUser? = null) {
    install(Authentication) {
        // 1.で作成した関数を呼び出し
        mock {
            // ログイン主体(Principal)の取得ロジックを記載
            principal { loginUser }
        }
    }
}
```

## 3. アプリケーションへの組み込み
モック認証を利用したいアプリケーション(のモジュール)に、モック認証のinstallerを追加します。
本番運用でモック認証は使わないでしょうから、
1. テスト時と本番利用時の実行モジュールを分けて定義
2. それぞれ別の認証installerを追加
とした上、テスト時には認証をモック化したモジュールを利用するようにします。

```kotlin
// テスト時に実行するモジュール
fun Application.testModule(loginUser: LoginUser? = null) {
    testAuth(loginUser)
    others()
}

// 本番利用時に実行するモジュール
fun Application.mainModule() {
    productionAuth() //mockでないauth
    others()
}

// 認証以外のモジュール
fun Application.others() {
    router()
    // ...and other modules
}

fun Application.router() {
    routing {
        authenticate {
            get("/hello") {
                val loginUser = call.principal<LoginUser>()!!
                // ...and some logics    
            }
        }
    }
}
```

## 4. 使う
あとは「テスト用のモジュール」の利用場面で、認証主体を指定するだけ。
以下は前掲のモジュールを利用したテストのサンプルです。*testModule*の引数に渡した「ログインユーザー(loginUser)」が、そのままルーティング上の*call.principal()*から取り出されます。

```kotlin
// Ktorのテストライブラリを利用してApiのテストをするサンプル
class Test {
    @Test
    fun `authenticated hello - success`() {
        // io.ktor.server.testing.withTestApplication
        withTestApplication({ testModule(loginUser = TestLoginUser.someone) }) {
            handleRequest(HttpMethod.Get, "/hello") { }
                .apply {
                    // ...some assertions
                }
        }
    }
}
```

# むすび
モック認証の都合が利用者側に露出しにくい(魔法のパラメータなどを仕込まなくていい)のは気に入っています。
認証をテスト時にモック化したいというニーズはそれなりに普遍的なはずが、情報が少ないのが意外…。ご参考までに。**ポン・チャガール**[^1]

[実装サンプル全体(GitHub)](https://github.com/satotats/ktor-auth-mocking-sample)


[^1]: [カプセルトイの歴史（日本語吹き替え版）](https://youtu.be/UzlDLNMirhE)より😇