KotlinでOAuth徹底入門のclientサーバーを実装する

OAuth徹底入門を読んでjsでclinetサーバーを実装したのでKotlinでも実装することでOAuthを利用する側の基礎を復習する。[githubにて公開](https://github.com/ginzzziro/kotlin-oauth)しているので基本的にはApplication.ktを参照してもらえれば問題ない。以下には実装で詰まった箇所、気づいた点をメモしていく。
#前提
Kotlin/jvm
Kotiln 1.3.50
ktor 1.2.4
authorization server,protected resource serverはOAuth徹底入門のch-3-1ディレクトリにあるものを使用する。
nodeの都合からauthorization serverが正常に動作しないので[githubのissue](https://github.com/oauthinaction/oauth-in-action-code/issues/26)を参考にする。2019/11/03時点で修正されていない。
#TOPページ
```Application.kt
    get("/") {
      call.respondHtml {
        body {
          h1 { +"OAuth徹底入門 with Kotlin" }
          a(href = "http://localhost:9000/authorize") { +"AUTHORIZE" }
        }
      }
    }
```
認証する前のためのhtmlを用意する。
#Authorization Codeを要求。
```Application.kt
    get("/authorize") {
      call.respondRedirect(false) {
        host = "localhost"
        port = 9001
        path("authorize")

        parameters["response_type"] = "code"
        parameters["client_id"] = ConstValue.CLIENT_ID
        parameters["redirect_uri"] = ConstValue.REDIRECT_URI

        val state = getRandomString()
        DB.states[ConstValue.CLIENT_ID] = state
        parameters["state"] = state
      }
    }
```
ポイントは上のstateパラメータです。OAuth徹底入門の7章にて解説されていますがCSRF対策のためにqueryパラメータの一つとして付与する必要がある。
また生成されるstateについては2^160以上のランダム性を持ったものが推奨されている。

```Application.kt
fun getRandomString(): String {
  return BigInteger(160, SecureRandom()).toString(32)
}
```
getRandomString関数は160桁の2進数で生成された数値を32進数の文字列に変換して生成している。
## ユーザーはAuthorization Serverが提供する認可UIを使用して許可
<img width="458" alt="スクリーンショット 2019-11-03 9.28.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229617/254c1e3a-2d76-a763-3d15-af1ff162980f.png">

```Application.kt
    get("/callback") {
      val code = call.request.queryParameters["code"]
      val state = call.request.queryParameters["state"]

      if (code == null || state == null || state != DB.states[ConstValue.CLIENT_ID]) {
        throw Exception("invalid authorization code")
      }
```
authorizationサーバーが叩くcallbackAPIにて受け取ったstateが攻撃者によってすり替えられたものではないのか検証する。
#tokenを要求
```Application.kt
      val req = Request.Builder().apply {
        val utf8 = StandardCharsets.UTF_8.toString()
        val clientIdByteArray = URLEncoder.encode(ConstValue.CLIENT_ID, utf8)
        val secretByteArray = URLEncoder.encode(ConstValue.CLIENT_SECRET, utf8)
        addHeader("Content-Type", "application/x-www-form-urlencoded")
        val idAndPass = Base64.encodeBase64String("$clientIdByteArray:$secretByteArray".toByteArray())
        addHeader("Authorization", "Basic $idAndPass")
        val body = "grant_type=authorization_code&code=$code&redirect_uri${ConstValue.REDIRECT_URI}".toRequestBody()
        method("POST", body)
        url("http://localhost:9001/token")
      }.build()
      val res = OkHttpClient().newCall(req).execute()
      val responseBody = Gson().fromJson(res.body!!.string(), TokenResp::class.java)!!
      ConstValue.TOKEN = responseBody.access_token

```
Authorization Code GrantではAuthorizationヘッダーを利用して認証することが推奨されている。これはtokenを交換する際も同様。
認証方法にてBasicが使用されている点とbodyがapplication/jsonではない点が慣れないが本質には関係ないので意識しない。
## tokenの取得に成功
<img width="453" alt="tokenの取得に成功" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229617/a2a75b3f-1b19-5f15-1873-5a8a39947a78.png">
#OAuth2にて保護されたリソースを要求
```Application.kt
      val req = Request.Builder().apply {
        addHeader("Content-Type", "application/x-www-form-urlencoded")
        addHeader("Authorization", "Bearer ${ConstValue.TOKEN}")
        url("http://localhost:9002/resource")
        method("POST", "".toRequestBody())
      }.build()
      val res = OkHttpClient().newCall(req).execute()
```
今回はトークンの種類としてBearerトークンが指定されているのでAuthorizationヘッダーにのvalueのprefixとして"Bearer "をつける。
Bearerトークンはbase64でエンコードされている必要がある。clientは何も確認せず使用することができる。
#まとめ
OAuth2は認可フローから作成されたフレームワークである。OpenIDConnectと同時に使用されることが多いので認証、認可を行えるものとして勘違いされるが今回使用した「Authorization Code Grant」を行うために最適化されたもの。これ以外にも認証フローはいくつか用意されているものの何かしらの脆弱性を許容していることを意識する。
clientサイドの実装を行う予定しか今のところないのでclientをkotlinにて実装したがそれ以外のサーバーを扱う必要があるときは今回のように別言語で実装し直してみると得られることが多い。