ScalaでOAuth1.0の署名を作る

Scala用のOAuth認証のライブラリを探してみると，通常の(他のユーザを専用のページへリダイレクトして承認してもらい，アクセストークン等を取得する)認証についてのものはいくつか見つかった．一方で，Twitterのような，自分のアクセストークンが分かっている場合の処理までREADMEに明記しているものは見つからなかった．
なので手動でAuthorizationヘッダを組み立てなければならないが，URLエンコードと暗号化でハマったので，メモしておく．
Java用のTwitterライブラリもあるが，Scalaライクなコードを書きたいので，あえて使わない(Scala初心者なので，改善点があれば教えてください)．

ここでは，URLエンコード，Base64エンコード，HMAC-SHA1での暗号化と，ついでにTwitterのためのAuthorizationヘッダの作り方を書いていく．

#実行環境

- macOS Sierra 10.12.2
- Scala 2.12.1
- sbt 0.13.13

#URLエンコード

java.net.URLEncoder.encode( _string_, _encoder_ ) を使う．_string_ にはエンコードしたい文字列を，_encoder_ には"UTF-8"を指定する．ただし，https://docs.oracle.com/javase/1.5.0/docs/api/java/net/URLEncoder.html によると，空白は，'%20'ではなく'+'へ変換されるので，replace("+", "%20")する必要がある:

```scala
def urlencode(s: String) = java.net.URLEncoder encode (s, "UTF-8") replace ("+", "%20")
```

#Base64エンコード

org.apache.commons.codec.binary.Base64.encodeBase64( _arrayOfBytes_ ) を使う．これはArray[Byte]を返す．`build.sbt`には

```scala
libraryDependencies ++= Seq(
  "commons-codec" % "commons-codec" % "1.3.0"
)
```

を書いておく．

#暗号化(HMAC-SHA1)

_key_, _data_ を引数として，以下のように書く．中身は理解していないが，http://technically.us/code/x/oauth-here-she-comes/ を参考にした．

```scala
def bytes(s: String) = s getBytes "UTF-8"

def encry(key: String, data: String) = {
  val hmacShai = "HmacSHA1"
  val keyStr = new javax.crypto.spec.SecretKeySpec(bytes(key), hmacShai)
  val mac = javax.crypto.Mac.getInstance(hmacShai)
  mac.init(keyStr)
  new String(org.apache.commons.codec.binary.Base64.encodeBase64(mac.doFinal(bytes(data))))
}
```

#署名の作成
以下，consumer key, consumer secret, token, token secretはすでに取得しているとし，これらをそれぞれ
`consumerKey, consumerSecret, token, tokenSecret` (すべてString)とする．また，MapはすべてMap[String, String]を表し，

```scala
def addOauth(m: Map) = m map { case (k, v) => ("oauth_" + k, v) }

def sortMap(m: Map) = m.toList sortBy {_._1}
```

という関数を定義する．importは以下の通り(play.apiはTwitterのAPIを叩くために必要):

```scala
import scala.concurrent.ExecutionContext.Implicits.global
import java.net.URLEncoder
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
import org.apache.commons.codec.binary.Base64.encodeBase64
import play.api.libs.json._
import play.api.libs.ws.ning.NingWSClient
```

`build.sbt` に以下を追加する:

```scala
libraryDependencies ++= Sep(
  "com.typesafe.play" %% "play-ws" % "2.4.0-M2"
)
```
timestampは `(System.currentTimeMillis / 1000).toString`，nonceは `System.nanoTime.toString` とする．

```scala
implicit val credentials = Map(
  "consumerKey" -> consumerKey,
  ...
  )
```

というように，consumerKey, consumerSecret, token, tokenSecretを並べる．署名(String)を返す関数 `calcSignature`は以下の通り(`httpMethod` は"GET"等):

```scala
def calcSignature(httpMethod: String, url: String, query: Map, timestamp: String, nonce: String)(implicit credentials: Map)= {
  val params = sortMap(addOauth(Map(
    "consumer_key"     -> credentials("consumerKey"),
    "nonce"            -> nonce,
    "signature_method" -> "HMAC-SHA1",
    "timestamp"        -> timestamp,
    "token"            -> credentials("token"),
    "version"          -> "1.0"
  )) ++ query )
  val paramString = params map { case (k, v) =>
    urlencode(k) + "=" + urlencode(v)
  } mkString "&"
  val signatureBase = httpMethod + "&" + urlencode(url) + "&" + urlencode(paramString)
  val signatureKey  = urlencode(credentials("consumerSecret")) + "&" + urlencode(credentials("tokenSecret"))
  encry(signatureKey, signatureBase)
}
```

#ヘッダの作成

あとは簡単．`timestamp` と `nonce` は適当な箇所で定義しておく．

```scala
def oauthHeader(httpMethod: String, url: String, query: Map)(implicit credentials: Map) = {
  val header = sortMap(addOauth(Map(
  "consumer_key"     -> credentials("consumerKey"),
  "token"            -> credentials("token"),
  "timestamp"        -> timestamp,
  "nonce"            -> nonce,
  "signature_method" -> "HMAC-SHA1",
  "signature"        -> calcSignature(httpMethod, url, query, timestamp, nonce),
  "version"          -> "1.0"
  )))
  "OAuth " + (header map { case (k, v) =>
    urlencode(k) + "=" + urlencode(v)
  } mkString ", ")
}
```

#まとめ
結局コードはこうなる:

- `build.sbt`

```scala
name := "twitter"

version := "1.0"

libraryDependencies ++= Seq(
  "com.typesafe.play" %% "play-ws" % "2.4.0-M2",
  "commons-codec" % "commons-codec" % "1.3.0"
)
```

- `twitter.scala`

```scala
import scala.concurrent.ExecutionContext.Implicits.global
import java.net.URLEncoder
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
import org.apache.commons.codec.binary.Base64.{encodeBase64 => base64encode}
import play.api.libs.json._
import play.api.libs.ws.WSResponse
import play.api.libs.ws.ning.NingWSClient

object Twitter {

  def main(args: Array[String]) = {
    // obtain these keys beforehand
    val consumerKey    = "AAA"
    val consumerSecret = "BBB"
    val token          = "CCC"
    val tokenSecret    = "DDD"
    implicit val credentials: Map[String, String] = Map(
      "consumerKey"    -> consumerKey,
      "consumerSecret" -> consumerSecret,
      "token"          -> token,
      "tokenSecret"    -> tokenSecret
    )

    // GET https://api.twitter.com/1.1/statuses/user_timeline.json?count=1&user_id=XXX
    val host = "https://api.twitter.com"
    val path = "/1.1/statuses/user_timeline.json"
    val url  = host + path

    val id = "XXX"
    val query: Map[String, String] = Map(
      "count"   -> "1",
      "user_id" -> id
    )

    val headers: Map[String, String] = Map(
      "Content-Type"  -> "application/x-www-form-urlencoded;charset=UTF-8",
      "Authorization" -> oauthHeader("GET", url, query)
    )

    // val ws = play.api.libs.ws.WSClient() dose not work outside the play framework,
    // so I use play.api.libs.ws.ning.NingWSClient(). why?
    implicit val ws: NingWSClient = NingWSClient()
    val res: Future[WSResponse] = get(url, query, headers)
    Thread.sleep(1000)
    println(res.value.get.get.json)
    ws.close()
  }

  def get(url: String, query: Map[String, String], headers: Map[String, String])(implicit ws: NingWSClient): Future[WSResponse] = {
    ws.url(url).withQueryString(query.toList:_*).withHeaders(headers.toList:_*).get
  }

  def oauthHeader(httpMethod: String, url: String, query: Map[String, String])(implicit credentials: Map[String, String]): String = {
    val consumerKey = credentials("consumerKey")
    val token       = credentials("token")
    val ts = timestamp
    val nc = nonce

    val header: List[(String, String)] = sortMap(addOauth(Map(
      "consumer_key"     -> consumerKey,
      "token"            -> token,
      "timestamp"        -> ts,
      "nonce"            -> nc,
      "signature_method" -> "HMAC-SHA1",
      "signature"        -> calcSignature(httpMethod, url, query, ts, nc),
      "version"          -> "1.0"
    )))
    "OAuth " + (header map { case (k, v) => urlencode(k) + "=" + urlencode(v) } mkString ", ")
  }


  private def calcSignature(httpMethod: String, url: String, query: Map[String, String], ts: String, nc: String)(implicit credentials: Map[String, String]): String = {
    val consumerKey    = credentials("consumerKey")
    val token          = credentials("token")
    val consumerSecret = credentials("consumerSecret")
    val tokenSecret    = credentials("tokenSecret")

    val signatureKey: String = urlencode(consumerSecret) + "&" + urlencode(tokenSecret)
    val params: List[(String, String)] = sortMap(addOauth(Map(
      "consumer_key"     -> consumerKey,
      "nonce"            -> nc,
      "signature_method" -> "HMAC-SHA1",
      "timestamp"        -> ts,
      "token"            -> token,
      "version"          -> "1.0"
    )) ++ query)
    val paramString: String = params map { case (k, v) => urlencode(k) + "=" + urlencode(v) } mkString "&"
    val signatureBase: String = httpMethod + "&" + urlencode(url) + "&" + urlencode(paramString)
    encry(signatureKey, signatureBase)
  }

  private def addOauth(m: Map[String, String]): Map[String, String] = {
    m map { case (k, v) => ("oauth_" + k, v) }
  }

  private def encry(key: String, data: String): Strng = {
    val hmacShai = "HmacSHA1"
    val keyStr: SecretKeySpec = new SecretKeySpec(bytes(key), hmacShai)
    val mac: Mac = Mac.getInstance(hmacShai)
    mac.init(keyStr)
    new String(base64encode(mac.doFinal(bytes(data))))
  }

  private def urlencode(s: String): String = URLEncoder encode (s, "UTF-8") replace ("+", "%20")

  private def bytes(s: String): String = s getBytes "UTF-8"

  private def timestamp(): String = (System.currentTimeMillis / 1000).toString

  private def nonce(): String = System.nanoTime.toString

  private def sortMap(m: Map[String, String]): List[(String, String)] = m.toList sortBy {_._1}
}
```

#参考リンク

- https://docs.oracle.com/javase/1.5.0/docs/api/java/net/URLEncoder.html
- http://technically.us/code/x/oauth-here-she-comes/
- https://dev.twitter.com/oauth/overview