Ktor + CognitoでOAuth2(OpenID Connect)認証

Ktorが1.0になってから久しく既に1.3のpreviewまで発展しているので個人開発アプリで使ってみました。
認証手段としてCognitoのOpenID Connect認証を使用したのですが微妙にはまりどころ合ったので共有します。
GoogleのAuth2認証やGithubの例については[公式](https://ktor.io/servers/features/authentication/oauth.html)に載っています。が、いかんせんKtor初心者にはわかりづらい書き方だったのでより詳細な例を使って実装することでわかりやすくしました。
今回の実装は[GitHubに公開](https://github.com/ginzro/ktor-oauth2-sample)してます。
# 対象
OAuth2,OpenID Connectについて大体理解してる
KtorでOAuth2使うとどうなる？な人
# 前提
Kotlin 1.3.50
Ktor 1.2.4
# 依存関係の追加
[Quick Start](https://ktor.io/quickstart/index.html)から何もオプションを選ばない状態から下の二行を追加する。

```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のチュートリアル](https://docs.aws.amazon.com/cognito/latest/developerguide/tutorial-create-user-pool.html)を参照する。
基本的にはデフォルト設定通りで問題ない。
app-clientを作成後、callback urlを設定する。
<img width="810" alt="スクリーンショット 2019-12-02 8.05.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229617/3d1a9c6b-2ff1-2eee-405c-91e7d0cd9f7f.png">
clientId,client secretを確認する。
<img width="825" alt="スクリーンショット 2019-12-02 8.05.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229617/68bea34e-e49d-a742-66db-3005ff833159.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を指定している](https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html)ので明示的に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エンドポイントへリクエストを投げてみます。
<img width="679" alt="スクリーンショット 2019-12-02 8.39.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229617/673887bb-81e6-7483-127f-00a8a6963093.png">
上手くいけば上の様にユーザーの情報を取得できます。

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