LoginSignup
6
4

More than 5 years have passed since last update.

ScalaでOAuth1.0の署名を作る

Last updated at Posted at 2017-01-07

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")する必要がある:

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には

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

を書いておく.

暗号化(HMAC-SHA1)

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

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]を表し,

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を叩くために必要):

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 に以下を追加する:

libraryDependencies ++= Sep(
  "com.typesafe.play" %% "play-ws" % "2.4.0-M2"
)

timestampは (System.currentTimeMillis / 1000).toString,nonceは System.nanoTime.toString とする.

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

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

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)
}

ヘッダの作成

あとは簡単.timestampnonce は適当な箇所で定義しておく.

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
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
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}
}

参考リンク

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4