Kotlin에서 OAuth 철저한 입문 클라이언트 서버 구현

12959 단어 OAuthKtorKotlin
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의 이슈 를 참고로 한다. 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를 사용하여 권한을 부여합니다.





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이 아닌 점이 익숙하지 않지만 본질에는 관계 없기 때문에 의식하지 않는다.

토큰 획득 성공





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에서 구현했지만 그 이외의 서버를 취급할 필요가 있을 때는 이번과 같이 다른 언어로 다시 구현해 보면 얻을 수 있는 경우가 많다 .

좋은 웹페이지 즐겨찾기