AndroidでOkHttp3/Retrofit2でOAuth認証する

OkHttp3/Retrofit2でOAuth認証が必要なAPIを使った時のメモです。接続先は[はてなさんのAPI](http://developer.hatena.ne.jp/ja/documents/bookmark/apis/rest
)です。なおサンプルはKotlinです。

### 準備
- おなじみのOkHttp/retrofitのセットに加えてOAuth用のライブラリを追加

```groovy:app/build.gradle
dependencies {

    ...

    compile 'com.squareup.okhttp3:okhttp:3.2.0'
    compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
    compile 'se.akerfeldt:okhttp-signpost:1.1.0'
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
    compile 'com.squareup.retrofit2:converter-gson:2.0.2'
}
```

### 初めて認証するとき
- 各サービスから発行されたconsumerKeyとconsumerSecretを使ってOkHttpOAuthConsumerのインスタンスを作ります。で、signメソッドにrequestを食わしてAuthorizationヘッダに認証用の情報を書き込むのですが、ライブラリのsignで書き込まれるパラメータは以下のとおりです。

    - oauth_consumer_key
    - oauth_signature_method
    - oauth_timestamp
    - oauth_nonce
    - oauth_token

- 上記以外の情報はsetAdditionalParametersで追加できます。下の例は認証後のCallbackのURLを追加で設定しています。

```
HttpParameters().apply {
    put("realm", "")
    put("oauth_callback", Uri.encode(CALLBACK))
}.let {
    consumer.setAdditionalParameters(it)
}
```

- 後は認可ページのリクエスト→認可→認可後のCallbackでoauthTokenとoauthTokenSecretを端末に保存しておけば次回からの認証付きリクエストに使えます。

### 認証付きのリクエスト
- 取得したoauthTokenとoauthTokenSecretをセットしたconsumerを作ってSigningInterceptorをOkHttpClientのInterceptorに追加してあげるとOAuth認証つきのリクエストになります

```kotlin
val consumer = OkHttpOAuthConsumer(consumerKey, consumerSecret)
consumer.setTokenWithSecret(oauthToken, oauthTokenSecret)

val client = HttpClientBuilder.instance.newBuilder()
        .addInterceptor(SigningInterceptor(consumer))
        .build()

val retrofit = Retrofit.Builder()
        .baseUrl("http://hogehoge")
        .client(client)
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .addConverterFactory(GsonConverterFactory.create())
        .build()
```

### おまけ
- ざっと作ったはてなOAuth認証用のクラス

```kotlin:HatenaOAuthManager.kt
class HatenaOAuthManager(private val consumerKey: String,
                         private val consumerSecret: String) {

    companion object {

        private val REQUEST_TOKEN_ENDPOINT_URL = "https://www.hatena.com/oauth/initiate"

        private val ACCESS_TOKEN_ENDPOINT_URL = "https://www.hatena.com/oauth/token"

        private val AUTHORIZATION_WEBSITE_URL = "https://www.hatena.ne.jp/touch/oauth/authorize"

        val AUTHORIZATION_DENY_URL = "$AUTHORIZATION_WEBSITE_URL.deny"

        val CALLBACK = "hogehoge"
    }

    var consumer = OkHttpOAuthConsumer(consumerKey, consumerSecret)
        private set
    
    // 認可ページのリクエスト
    fun retrieveRequestToken(): String? {

        consumer = OkHttpOAuthConsumer(consumerKey, consumerSecret)
        HttpParameters().apply {
            put("realm", "")
            put(OAuth.OAUTH_CALLBACK, Uri.encode(CALLBACK))
        }.let {
            consumer.setAdditionalParameters(it)
        }

        val authorizeUrl = HttpUrl.parse(REQUEST_TOKEN_ENDPOINT_URL)
                .newBuilder()
                .addQueryParameter("scope", "read_public,write_public")
                .build()

        val request = Request.Builder()
                .url(authorizeUrl)
                .build()

        val signedRequest = consumer.sign(request).unwrap() as Request

        val response = HttpClient.instance.newCall(signedRequest).execute()

        if (response.code() != HttpURLConnection.HTTP_OK) {
            return null
        }

        var oauthToken: String = ""
        var oauthTokenSecret: String = ""

        response.body().string().split("&").forEach {
            val param = it.split("=")
            when (param[0]) {
                OAuth.OAUTH_TOKEN -> oauthToken = Uri.decode(param[1])
                OAuth.OAUTH_TOKEN_SECRET -> oauthTokenSecret = Uri.decode(param[1])
            }
        }

        if (oauthToken.isBlank() || oauthTokenSecret.isBlank()) {
            return null
        }

        consumer.setTokenWithSecret(oauthToken, oauthTokenSecret)

        return HttpUrl.parse(AUTHORIZATION_WEBSITE_URL)
                .newBuilder()
                .addQueryParameter(OAuth.OAUTH_TOKEN, oauthToken)
                .build().toString()
    }

    // 認可後のトークン取得のリクエスト
    fun retrieveAccessToken(requestToken: String): Boolean {

        var oauthToken = consumer.token

        var oauthTokenSecret = consumer.tokenSecret

        consumer = OkHttpOAuthConsumer(consumerKey, consumerSecret)

        consumer.setTokenWithSecret(oauthToken, oauthTokenSecret)

        HttpParameters().apply {
            put("realm", "")
            put(OAuth.OAUTH_VERIFIER, Uri.encode(requestToken))
        }.let {
            consumer.setAdditionalParameters(it)
        }

        val authorizeUrl = HttpUrl.parse(ACCESS_TOKEN_ENDPOINT_URL)
                .newBuilder()
                .build()

        val request = Request.Builder()
                .url(authorizeUrl)
                .build()

        val signedRequest = consumer.sign(request).unwrap() as Request

        val response = HttpClient.instance.newCall(signedRequest).execute()

        if (response.code() != HttpURLConnection.HTTP_OK) {
            consumer.setTokenWithSecret(null, null)
            return false
        }

        response.body().string().split("&").forEach {
            val param = it.split("=")
            when (param[0]) {
                OAuth.OAUTH_TOKEN -> oauthToken = Uri.decode(param[1])
                OAuth.OAUTH_TOKEN_SECRET -> oauthTokenSecret = Uri.decode(param[1])
            }
        }

        if (oauthToken.isBlank() || oauthTokenSecret.isBlank()) {
            consumer.setTokenWithSecret(null, null)
            return false
        }

        consumer.setTokenWithSecret(oauthToken, oauthTokenSecret)
        return true
    }
}
```