OAuth徹底入門を読んでjsでclinetサーバーを実装したのでKotlinでも実装することでOAuthを利用する側の基礎を復習する。githubにて公開しているので基本的には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を参考にする。2019/11/03時点で修正されていない。
TOPページ
get("/") {
call.respondHtml {
body {
h1 { +"OAuth徹底入門 with Kotlin" }
a(href = "http://localhost:9000/authorize") { +"AUTHORIZE" }
}
}
}
認証する前のためのhtmlを用意する。
Authorization Codeを要求。
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以上のランダム性を持ったものが推奨されている。
fun getRandomString(): String {
return BigInteger(160, SecureRandom()).toString(32)
}
getRandomString関数は160桁の2進数で生成された数値を32進数の文字列に変換して生成している。
ユーザーはAuthorization Serverが提供する認可UIを使用して許可

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を要求
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の取得に成功
