RubyでOAuth

最近、Rubyを弄ってなくすっかり忘れてしまったのでtoRuby勉強会に向けてのリハビリを兼ねて、昔書いたTwitterライブラリをOAuthに対応させてみた。
OAuthは既存のライブラリを使うのではなく id:yoshiori さんのブログ
やる夫と Python で学ぶ Twitter の OAuth - 宇宙行きたい
が分かり易くてとても参考になったのでコードをパクらせて頂いたw

# -*- coding: utf-8 -*-
#
# oauth.rb
#  Twitter用OAuthライブラリ
#
require "openssl"
require "base64"
require "uri"
require "net/http"

class OAuth
  URL = "http://twitter.com"
  REQUEST_TOKEN_PATH = "/oauth/request_token"
  ACCESS_TOKEN_PATH = "/oauth/access_token"
  OAUTH_VERIFIER_PATH = "/oauth/authorize"

  # OAuth spec escape
  def escape(str)
    URI.escape(str, /[^a-zA-Z0-9\-\.\_\~]/)
  end
  private :escape

  # パラメータをエンコードした key=value の形にして separator で繋げる
  def encode_params(hash, separator)
    array = []
    hash.each do |key, value|
      array.push escape(key) + "=" + escape(value)
    end
    array.join(separator)
  end
  private :encode_params

  # キーを元に message で hmac-digest を作成し base64 でエンコード
  def signature(key, message)
    Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)).strip
  end
  private :signature

  # nonce word
  def nonce
    OpenSSL::Digest::Digest.hexdigest("MD5", "#{Time.now.to_f}#{rand}")
  end
  private :nonce

  # timestamp
  def timestamp
    Time.now.tv_sec.to_s
  end
  private :timestamp

  # parse
  def parse_body(body)
    hash = {}
    body.split("&").each do |param|
      key, value = param.split("=", 2)
      hash[key] = value
    end
    hash
  end
  private :parse_body

  def request_get(path, header = nil)
    http = Net::HTTP.new(URI.parse(URL).host)
    http.request_get(path, header)
  end
  private :request_get

  def initialize(consumer_key, consumer_secret, oauth_token = nil, oauth_token_secret = nil)
    @consumer_key = consumer_key
    @consumer_secret = consumer_secret

    # 必須パラメータ
    @params = {
      "oauth_consumer_key" => consumer_key,
      "oauth_signature_method" => "HMAC-SHA1",
      "oauth_version" => "1.0",
    }

    if oauth_token != nil && oauth_token_secret != nil
      @params["oauth_token"] = oauth_token
      @oauth_token = oauth_token
      @oauth_token_secret = oauth_token_secret
    end
  end

  # Request Token取得
  def request_token
    params = @params.dup
    params["oauth_timestamp"] = timestamp
    params["oauth_nonce"] = nonce

    # パラメータをソートし,エンコードした key=value の形にして & で繋げる
    params_str = encode_params(params.sort, "&")

    # メソッド, エンコードした URL, 上で作ったパラメータ文字列を & で繋げる
    message = "GET&" + escape(URL + REQUEST_TOKEN_PATH) + "&" + escape(params_str)

    # consumer_secret を元にキーを作成
    key = @consumer_secret + "&"

    # キーを元に message で hmac-digest を作成
    sig = signature(key, message)

    # 作成したダイジェストをパラメータに追加
    params["oauth_signature"] = sig

    # 作成したパラメータを GET のパラメータとして追加
    path = REQUEST_TOKEN_PATH + "?" + encode_params(params, "&")

    res = request_get(path)
    parse_body(res.body)
  end

  # Access Token取得
  def access_token(oauth_token, oauth_token_secret, oauth_verifier)
    params = @params.dup
    params["oauth_token"] = oauth_token
    params["oauth_token_secert"] = oauth_token_secret
    params["oauth_verifier"] = oauth_verifier
    params["oauth_timestamp"] = timestamp
    params["oauth_nonce"] = nonce

    # パラメータをソートし,エンコードした key=value の形にして & で繋げる
    params_str = encode_params(params.sort, "&")

    # メソッド, エンコードした URL, 上で作ったパラメータ文字列を & で繋げる
    message = "GET&" + escape(URL + ACCESS_TOKEN_PATH) + "&" + escape(params_str)

    # consumer_secret を元にキーを作成
    key = @consumer_secret + "&" + oauth_token_secret

    # キーを元に message で hmac-digest を作成
    sig = signature(key, message)

    # 作成したダイジェストをパラメータに追加
    params["oauth_signature"] = sig

    # 作成したパラメータを GET のパラメータとして追加
    path = ACCESS_TOKEN_PATH + "?" + encode_params(params, "&")

    # ヘッダに Authorization:OAuth を追加
    header = {"Authorization" => "OAuth"}

    res = request_get(path, header)
    parse_body(res.body)
  end

  # OAuth Verifier URL
  def oauth_verifier_url(token)
    URL + OAUTH_VERIFIER_PATH + "?oauth_token=" + token["oauth_token"]
  end

  # Authorization Header
  def auth_header(method, url, option = nil)
    params = @params.dup
    params["oauth_timestamp"] = timestamp
    params["oauth_nonce"] = nonce

    # option をパラメータに追加
    opt_key = nil
    if option != nil
      opt_key, value = option.split("=", 2)
      params[opt_key] = value
    end

    # パラメータをソートし,エンコードした key=value の形にして & で繋げる
    params_str = encode_params(params.sort, "&")

    # メソッド, エンコードした URL, 上で作ったパラメータ文字列を & で繋げる
    message = method.to_s.upcase + "&" + escape(url) + "&" + escape(params_str)

    # consumer_secret を元にキーを作成
    key = @consumer_secret + "&" + @oauth_token_secret

    # キーを元に message で hmac-digest を作成
    sig = signature(key, message)

    # 作成したダイジェストをパラメータに追加
    params["oauth_signature"] = sig

    # header に不要なパラメータを削除
    if option != nil
      params.delete(opt_key)
    end

    header = {"Authorization" => "OAuth " + encode_params(params, ",")}
  end
end

OAuthコア以外のソースはこちら
GitHub - m92o/twitter-ruby: Twitter library for Ruby


アクセストークンはこんな風にして取得します。
1. Twitter Application Management でアプリ登録してConsumer Key / Consumer Secretを得る
2. Consumer Key / Consumer Secretを元にOAuth Token / OAuth Token Secret (リクエストークン)及びOAuth Verifier URLを得る

$ ruby requesttoken.rb consumer_key consumer_secret

3. OAuth Verifier URL にブラウザでアクセスしてアプリ使用許可しOAuth Verifierを得る
4. 今まで取得した情報を元にOAuth Token / OAuth Token Secret (アクセストークン)を得る

$ ruby requesttoken.rb consumer_key consumer_secret oauth_token oauth_token_secret oauth_verifier


つぶやくのはこんな感じです。

# -*- coding: utf-8 -*-
require 'twitter'

# 発言内容
text = 'おっぱい'

consumer_key = '最初に使ったやつ'
consumer_secret = '最初に使ったやつ'
oauth_token = 'アクセストークン取得の時に取得した OAuth Token'
oauth_token_secret = 'アクセストークン取得の時に取得した OAuth Token Secret'

ssl = false

tw = Twitter.new(consumer_key, consumer_secret, oauth_token, oauth_token_secret, ssl)
tw.update(text)

ちょっとバグってて発言内容にイコール「=」が入ってると認証エラーになっちゃいます。何故だろう?