KotlinでOAuth徹底入門のclientサーバーを実装する
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ページ
Application.kt
get("/") {
call.respondHtml {
body {
h1 { +"OAuth徹底入門 with Kotlin" }
a(href = "http://localhost:9000/authorize") { +"AUTHORIZE" }
}
}
}
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
}
}
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を要求
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
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の取得に成功
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()
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にて実装したがそれ以外のサーバーを扱う必要があるときは今回のように別言語で実装し直してみると得られることが多い。
Author And Source
この問題について(KotlinでOAuth徹底入門のclientサーバーを実装する), 我々は、より多くの情報をここで見つけました https://qiita.com/ginzro/items/6f5e9aa64e252d8f3faa著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .