Ktor + CognitoでOAuth2(OpenID Connect)認証

nomoa,Kotlinoauth2cognito

Ktorが1.0になってから久しく既に1.3のpreviewまで発展しているので個人開発アプリで使ってみました。 認証手段としてCognitoのOpenID Connect認証を使用したのですが微妙にはまりどころ合ったので共有します。 GoogleのAuth2認証やGithubの例については公式 (opens in a new tab)に載っています。が、いかんせんKtor初心者にはわかりづらい書き方だったのでより詳細な例を使って実装することでわかりやすくしました。 今回の実装はGitHubに公開 (opens in a new tab)してます。

OAuth2,OpenID Connectについて大体理解してる KtorでOAuth2使うとどうなる?な人

Kotlin 1.3.50 Ktor 1.2.4

Quick Start (opens in a new tab)から何もオプションを選ばない状態のbuild.gradle.ktsから下の二行を追加する。

    // https://mvnrepository.com/artifact/io.ktor/ktor-auth
    compile "io.ktor:ktor-auth:$ktor_version"

    // https://mvnrepository.com/artifact/io.ktor/ktor-client-apache
    compile "io.ktor:ktor-client-apache:$ktor_version"

ktor-authはOAuth2認証を使用するため、ktor-client-apacheは認証フローの際にCognitoと通信するために使用する。

Cognitoのチュートリアル (opens in a new tab)を参照する。 基本的にはデフォルト設定通りで問題ない。 app-clientを作成後、callback urlを設定する。 setting-image1 setting-image2

以下が全体のコードです。

// Application.kt
fun Application.module(testing: Boolean = false) {
    install(Authentication) {
        oauth(COGNITO) {
            client = HttpClient()
            providerLookup = {
                val domain = getEnv("cognito.domain")
                OAuthServerSettings.OAuth2ServerSettings(
                    name = "cognito",
                    authorizeUrl = "$domain/oauth2/authorize",
                    accessTokenUrl = "$domain/oauth2/token",
                    requestMethod = HttpMethod.Post,
                    clientId = getEnv("cognito.clientId"),
                    clientSecret = getEnv("cognito.clientSecret")
                )
            }
            urlProvider = { "http://localhost:8080/login" }
        }
    }
    routing {
        authenticate(COGNITO) {
            get("/login") {
                val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
                if (principal == null) {
                    call.respondRedirect("http://localhost:8080/")
                } else {
                    val domain = getEnv("cognito.domain")
                    val client = HttpClient()
                    val result = client.get<String>("$domain/oauth2/userInfo") {
                        header("Authorization", "Bearer ${principal.accessToken}")
                    }
                    call.respondHtml {
                        body {
                            h1 { +"you are login." }
                            a { +result }
                        }
                    }
                }
            }
        }

        get("/") {
            call.respondHtml {
                head {
                    title { +"Login with" }
                }
                body {
                    h1 { +"Login with:" }
                    a(href = "/login") { +"cognito" }
                }
            }
        }
    }
}

詳細に分けて見ていきます。

OAuth2設定

    install(Authentication) {
        // 中略
    }
const val COGNITO = "cognito"

まず初めにKtorに認証機能の使用を宣言します。

        oauth(COGNITO) {
            client = HttpClient()
            providerLookup = { 
                // 中略 
            }
            urlProvider = { "http://localhost:8080/login" }
        }      

認証module中でもOAuth2機能を使用していきます。oauth以外にもbasic,formを使用できます。

            providerLookup = {
                val domain = getEnv("cognito.domain")
                OAuthServerSettings.OAuth2ServerSettings(
                    name = "cognito",
                    authorizeUrl = "$domain/oauth2/authorize",
                    accessTokenUrl = "$domain/oauth2/token",
                    requestMethod = HttpMethod.Post,
                    clientId = getEnv("cognito.clientId"),
                    clientSecret = getEnv("cognito.clientSecret")
                )
            }

Authorization Serverの設定をしていきます。基本的に引数名通りですがrequestMethodはdefaultではGetが設定sらえており今回使用するCognitoはtokenリクエストの際にPost methodを指定している (opens in a new tab)ので明示的にrequestMethodを指定します。

これでOAuth2の設定は完了しました。

Routing設定

routing {
    authenticate(COGNITO) {
        get("/login") {
            // 中略
        }
    }

    get("/") {
        // 中略
    }
}

認証をかけたい場所に先ほど設定した認証名を利用してauthenticateでwrapします。/loginにアクセスした際には認証されているかどうか確認してOAuth2認証を行ってくれます。なんでもdsl記法になっているのがKtorっぽいですね。

                val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
                if (principal == null) {
                    call.respondRedirect("http://localhost:8080/")
                } else {
                    val domain = getEnv("cognito.domain")
                    val client = HttpClient()
                    val result = client.get<String>("$domain/oauth2/userInfo") {
                        header("Authorization", "Bearer ${principal.accessToken}")
                    }
                    call.respondHtml {
                        body {
                            h1 { +"you are login." }
                            a { +result }
                        }
                    }
                }

後はroute内で自動で取得されたprincipalを使用して実際に使用できるかuserInfoエンドポイントへリクエストを投げてみます。

上手くいけば上の様にユーザーの情報を取得できます。 setting-image3

今回はaccess tokenを取得しただけなので実際にユーザー認証として使用するためにはjwtを使用する必要がありますが内容複雑化すると判断したので辞めました。Spring BootのOAuth2認証 (opens in a new tab)も試しましたがそちらより複雑さが少なく良いです。 後、HTML DSLがいいですね!。今回の様に簡単なHTMLを必要とする実装をコード上からサクッとかけると言うのはプロトタイピングがよりやりやすくなっていることを感じます。実際に個人開発で使用していますが画面フローの確認に関してはこれを使ってある程度やれるので気に入ってます。

© nomoa.devRSS