KtorでMock Authentication(モック認証)


認証を モック化したい ことがある

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

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

1. モック認証用の拡張関数の作成

KtorのAuthenticationプラグインにデフォルトで実装されているformbasicといった関数を参考に、セットした認証主体の情報を返すだけの拡張関数を作成します。
実装すべきは大きく2つ、①Authentication.Configurationの拡張関数と、②拡張関数中で利用するAuthenticationProviderの継承クラスです。

// 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)
}
// 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()から取得されます。
利用ごとに認証情報を切り替えたい場合は、関数の引数などから情報を差し込めるようにしておきます。以下、サンプルです。

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

3. アプリケーションへの組み込み

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

// テスト時に実行するモジュール
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()から取り出されます。

// 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)