KtorのAuthを使う [Form POST編]

`Ktor`の認証処理のための`Auth feature`を少し触ってみたので書いていきます。

FormをPOSTして認証するパターンと、OAuthのパターンを書こうと思ったけど、思ったより長くなりそうなので分けます。

KotlinのVersionは`1.2.0`、`Ktor`のVersionは`0.9.0`です。
`Auth`の他に、`Html builder`を使用します。

KtorでのHello worldまでは[この記事](https://qiita.com/KissyBnts/items/1e1caa636941391a380e)に書きましたので、アプリケーションの起動まではそっちを見てください。

# 事前準備

`build.gradle`に依存関係を追加します。
今回追加するのは、`Ktor`の提供する`Auth`, `Html builder`です。


```groovy:build.gradle
repositories {
    ....
    jcenter()
}

dependencies {
    ....
    compile "io.ktor:ktor-auth:$ktor_version"
    compile "io.ktor:ktor-html-builder:$ktor_version"
    ....
}
```

`jcenter`は、`Html builder`を依存関係に追加する時に必要となります。`Html builder`を使わない場合は、必要ありません。

installもしておきます。

```kotlin:Application.kt
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.routing.Routing

fun Application.main() {
    ...
    install(Routing) {}
}
```

# FormをPostして認証する

メールアドレスとパスワードで認証する簡単なページを作ります。
URLは`/form/login`とし、`FormPost.kt`というファイルを作って書いていきます。

## Routeを作る

`io.ktor.routing.Route`の拡張関数を書いていきます。

```kotlin:FormPost.kt
import io.ktor.routing.Route
import io.ktor.routing.route

fun Route.formPost() {
    route("/form/login") {

    }
}
```

`Application.main()`で呼び出します。

```kotlin:Applcation.kt

fun Application.main() {
    ...
    install(Routing) {
        formPost()
    }
}
```


## GET時の処理を書く

```kotlin:FormPost.kt
import io.ktor.application.call
import io.ktor.html.respondHtml
import io.ktor.routing.get
import kotlinx.html.*

fun Route.formPost() {
    route("/form/login") {
        get {
            call.respondHtml {
                body {
                    form(action = "/form/login", method = FormMethod.post) {
                        p {
                            +"Email: "
                            textInput(name = "email") { value = "" }
                        }
                        p {
                            +"Password: "
                            passwordInput(name = "password") { value = "" }
                        }
                        p {
                            submitInput { value = "Login" }
                        }
                    }
                }
            }
        }
    }
}
```

Htmlを組み立てているので見慣れないとちょっと気持ち悪いかもですが、フォームを作っているだけです。
メールアドレスのinputを`email`、パスワードのinputを`password`とします。POST先は同じく`/form/login`です。

`/form/login`にアクセスすると以下のようなページが表示されます。
<img width="303" alt="form_login_get.png" src="https://qiita-image-store.s3.amazonaws.com/0/86504/502f569f-58bc-43f0-c97b-9959774275eb.png">

## 認証の処理を書く

認証の処理を書く前にログイン可能なユーザーを定義しておきます。
データベースを使うのは面倒なので、今回は簡単なユーザークラスのリストで代用します。

```kotlin:FormPost.kt
fun Route.formPost() { ... }

data class FormPostUser(val name: String, val email: String, val password: String)

// ログイン可能なユーザーリスト
val userList = listOf(
    FormPostUser("hoge", "hoge@example.com", "hoge"),
    FormPostUser("piyo", "piyo@example.com", "piyopiyo")
)
```

POSTされた`email`と`password`が正しいかを検証する処理を追加します。

```kotlin:FormPost.kt
import io.ktor.auth.*
import io.ktor.http.HttpMethod
import io.ktor.routing.method

fun Route.formPost() {
    route("/form/login") {
        get { ... }

        method(HttpMethod.Post) {
            authentication {
                formAuthentication(userParamName = "email") { upc: UserPasswordCredential ->
                    userList.find { it.email == upc.name && it.password == ucp.password }?.let { UserIdPrincipal(it.name) }
                }
            }

            handle { TODO() }
        }
    }
}
```

`authentication`というパイプラインを追加することで、リクエストを処理する前に認証処理を挟むことができます。
一般的なFormを使った一意なデータ(email)とパスワードの組み合わせでの認証ですので`formAuthentication`を使用します。

`formAuthentication`の定義は、

```kotlin
fun AuthenticationPipeline.formAuthentication(userParamName: String = "user",
                                              passwordParamName: String = "password",
                                              challenge: FormAuthChallenge = FormAuthChallenge.Unauthorized,
                                              validate: (UserPasswordCredential) -> Principal?) { ... }
```

となっており、

- `userParamName`：一意なデータのパラメータ名
- `passwordParamName`：パスワードのパラメータ名
- `challenge`：認証失敗時の処理
- `validate`：認証の処理

を渡すことができます。

今回は一意なデータのパラメータ名はデフォルト引数の`user`ではなく`email`なので、`formAuthentication(userParamName = "email")`と指定する必要があります。

`validate`は、パラメータをマッピングした`name`と`password`フィールドを持つ`UserPasswordCredential`クラスを引数とする関数で、ここに認証処理を書いていきます。

`Auth feature`では、認証情報を`Principal`インターフェースを実装したクラスで扱うので、リクエストされた`email`と`password`一致するユーザーを探し、`UserIdPrincipal`というクラスを返しています。
認証失敗時は、`null`を返します。


`authentication`パイプラインの中では、`formAuthentication`の他に、`basicAuthentication`や`oatuhAtLocation`などが定義されています。また、独自の認証処理を追加することもできます。(後述)

## POST時の処理を書く

```kotlin:FormPost.kt
fun Route.formPost() {
    route("/form/login") {
        get { ... }

        method(HttpMethod.Post) {
            authentication { ... }

            handle {
                val principal = call.principal<UserIdPrincipal>()?: throw IllegalStateException("Principal is null.")

                call.respondHtml {
                    body {
                        h1 {
                            +"Hello, ${principal.name}"
                        }
                    }
                }
            }
        }
    }
}
```

POST時の本処理は`handle`の中に書いていきます。

`ApplicationCall.principal<T : Principal>()`で`formAuthentication`の`valid`で返した認証情報を取得することができます。
`　UserIdPrincipal`はコンスタラクタで渡される`name`だけを持つdata classなので、それを使ってHtmlを返しています。

ちなみに、ここで`ApplicationCall.principal<T : Principal>()`がnullを返すパターンはないはず。
認証が失敗して`valid`でnullを返した場合、`formAuthentication`がレスポンスを返してしまいます。

POST時は
`method(HttpMethod.Post){ handle { ... } }`
としないといけないのが面倒に見えると思いますがこれは認証処理を挟むためで、`io.ktor.routing.get`は

```kotlin
fun Route.get(body: PipelineInterceptor<Unit, ApplicationCall>): Route {
    return method(HttpMethod.Get) { handle(body) }
}
```
となっており、やってることは同じです。ただ、`authentication`や他の`intercept`などを挟むことができません。

## 認証してみる

適切なメールアドレスとパスワードを入力しLoginすると、以下のように表示されます。

<img width="336" alt="form_login_success.png" src="https://qiita-image-store.s3.amazonaws.com/0/86504/cbf018c1-ef46-21ca-cafd-c9efadad2120.png">
別ページにリダイレクトさせる場合は`call.respondRedirect()`を使うのかもしれません。
セッションまわりは`Sessions`というfeatureがあるのでそちらでやるのだろうなぁと思っています。

今の実装だと、認証に成功した場合は問題ないですが、認証に失敗すると`401 unauthorized`を返すだけで、空白のページに遷移してしまいます。
これを少し修正して、失敗したときはログインページに戻してエラーメッセージを表示するようにします。

## 認証失敗時の処理を書く

`AuthenticationPipeline.formAuthentication`には`challenge`という引数があり、これで失敗時の処理を指定できます。

```kotlin:FormPost.kt
fun Route.formPost() {
    route("/form/login") {
        ...

        method(HttpMethod.Post) {
            authentication {
                formAuthentication(userParamName = "email",
                                   challenge = FormAuthChallenge.Redirect { call, _ ->  
                                       call.request.uri + "?error=Invalid credential"
                                   }) { 
                     ... 
                 }
            }

　　　　　　　　　　　　　　　　　　　　　　　 ...
        }
    }
}
```

デフォルトでは`401 unauthorized`を返すようになっていますが、ログインページに`error`というパラメータをつけてリダイレクトするようにしました。

あとはGET時に`error`パラメータはある時に、エラーメッセージを表示するように変更するだけです。

```kotlin:FormPost.kt
fun Route.formPost() {
    route("/form/login") {
        get {
            // 追加
            val error = call.parameters["error"]

            call.respondHtml {
                body {
                    form(action = "/form/login", method = FormMethod.post) {
                        // 追加
                        if (error != null) {
                            p {
                                +"Error: $error"
                            }
                        }

                        p {
                            +"Email: "
                            textInput(name = "email") { value = "" }
                        }
                        p {
                            +"Password: "
                            passwordInput(name = "password") { value = "" }
                        }
                        p {
                            submitInput { value = "Login" }
                        }
                    }
                }
            }
        }

        ...
    }
}

```

誤った情報でログインしようとするとこのような表示になります。

<img width="490" alt="form_login_failed.png" src="https://qiita-image-store.s3.amazonaws.com/0/86504/6deb7a0b-5d75-5da7-5258-d691a3278a54.png">

ちなみに、`challenge`は`FormAuthChallenge`型となっており、`FormAuthChallenge`は、

```kotlin
sealed class FormAuthChallenge {
    class Redirect(val url: (ApplicationCall, UserPasswordCredential?) -> String) : FormAuthChallenge()
    object Unauthorized : FormAuthChallenge()
}
```
と定義されているため、`401 unauthorized`を返すかリダイレクトするかの2択のようです。

-----

次はGItHubのAPIを使ってOAuth認証するのを書きます。
`Sessions`は現状さわる予定なし。