Help us understand the problem. What is going on with this article?

Ktor + CognitoでOAuth2(OpenID Connect)認証

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

対象

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

前提

Kotlin 1.3.50
Ktor 1.2.4

依存関係の追加

Quick Startから何もオプションを選ばない状態から下の二行を追加する。

build.gradle
    // 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

Cognitoのチュートリアルを参照する。
基本的にはデフォルト設定通りで問題ない。
app-clientを作成後、callback urlを設定する。
スクリーンショット 2019-12-02 8.05.10.png
clientId,client secretを確認する。
スクリーンショット 2019-12-02 8.05.21.png

サーバーサイド(OAuth2視点ではclient)

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

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設定

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

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

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

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

  • providerLookup
    Authorization Serverの設定
  • urlProvider
    callbackURL
Application.kt
            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を指定しているので明示的にrequestMethodを指定します。

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

Routing設定

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

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

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

Applicaiton.kt
                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エンドポイントへリクエストを投げてみます。
スクリーンショット 2019-12-02 8.39.37.png
上手くいけば上の様にユーザーの情報を取得できます。

まとめ

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした