RubyでTwitterのOAuth認証をしてみる

[Ruby][Twitter][API]RubyでTwitterのOAuth認証をしてみる

随分前にTwitterがベーシック認証からOAuth認証に切り替えたという事で。Java+Wicket+AppEngineでベーシック認証を駆使して作っていたTwitterサイトが見れなくなったもんで切り替えました。(http://gihyo.jp/dev/feature/01/wicket:title を見つつ)

一応出来たんですが、結局のところOAuthがどうなってるのかよくわからなかった……ので、ちょっと一から書いてみようと。

id:Yoshiori さんのhttp://d.hatena.ne.jp/Yoshiori/20100929/1285727199:title と、 id:yuroyoro さんの[http://d.hatena.ne.jp/yuroyoro/20100506/1273137673:title]がとっても詳しかったので、参考にしました

大きな流れとしては、

  1. consumer_keyとconsumer_secretを発行してもらう
  2. リクエストトークンを発行してもらう
  3. アクセストークンを発行してもらう

の3項目。今回はリクエストトークンを発行してもらうところまでやります。

準備 Twitterにアプリを登録し、consumer_keyとconsumer_secretを発行してもらう

  • Twitterにログイン > 設定 > 連携アプリ を選択

[f:id:kk_Ataka:20101120225746p:image]

  • 開発者の方へ > こちら を選択

[f:id:kk_Ataka:20101120230042p:image]

  • ページ下部の新しいアプリケーションを追加 を選択

[f:id:kk_Ataka:20101120230314p:image]

後で編集もできるのでとりあえず入力しておく。下記の2項目はとりあえず

  • - アプリケーションの種類: クライアントアプリケーション
    • 標準のアクセスタイプ: Read & Write

にしておく。

  • 登録したら、consumer_keyとconsumer_secretをもらえるので控えておく

[f:id:kk_Ataka:20101120232314p:image]

フォローをリクエストしました。のURL、Access token URL、Authorize URLは認証時に使うのでこれも控えておく。

リクエストトークンを発行してもらう

こっからRuby。以下のパラメータを生成してhttp://twitter.com/oauth/request_tokenに送ります。POSTでもGETでもよいみたいなので、今回はGETを使ってURLのおしりにくっつけて送ります

|*oauth_consumer_key|Twitterからもらったconsumer_key|

|*oauth_nonce|一意な値(にする必要があるが、とりあえず適当でもよいみたい)|

|*oauth_signature|認証するための暗号|

|*oauth_signature_method|認証方式(色々あるようだが、Twitterでは”HMAC-SHA1”固定)|

|*oauth_timestamp|今のタイムスタンプ(ミリ秒)|

|*oauth_version|バージョン(必須ではないが、付ける場合は”1.0”)|

consumer_key, nonce, signature_method, timestamp, versionの生成は難しくないのですが、問題はsignature。signature生成は大きく3つの流れを踏む事になります。

  • 認証用の値を生成する(以下の3つの値を&で連結する←この&はエスケープしない)
  1. 1. http_methodの種類(“GET”か”POST”。今回は”GET”)
    1. http://twitter.com/oauth/request_token”をエスケープしたもの
    2. 上記のパラメータからoauth_signatureを抜かしたパラメータアルファベット順に並べてxxx=yyy&vvv=zzz……の形で連結した値をエスケープしたもの
  • 署名キーを生成する
  1. 1. リクエストトークンを発行してもらうときは“consumer_secret&”(consumer_secretのおしりに&を連結する)
  • キーを元に値をHMAC-SHA1方式で暗号化した値をbase64形式でエンコードする

……認証に失敗したとき、どこのステップで間違ってるのかわからなかったので非常に苦労しました。幸いsignatureを生成してくれるページ http://cgi.geocities.jp/ydevnet/techblog/sample/signature.html:title があるので、ここで作成した値と同じ状況を作って比較しました。

|*OAuth type|2-legged OAuth|

|*URL|http://twitter.com/oauth/request_token|

|*parameters|なし|

|*consumer key|Twitterからもらったconsumer_key|

|*consumer secret|Twitterからもらったconsumer_secret|

|*version|1.0|

|*timestamp|nowを押して発行されたtimestampをソースに逆移植する|

|*nonce|randomを押して発行されたnonceをソースに逆移植する|

|*signature method|HMAC-SHA1固定|

これでsignして生成された値のうち、signature base stringが「認証用の値を生成する」で生成したかった値。signatureが「キーを元に値をHMAC-SHA1方式で暗号化した値をbase64形式でエンコードする」で生成したかった値となっている。あとはがんばる!

ソースコード

コードはこんな感じで…かなり泥臭く実装; 基本的に上から下に流れていきますが文字列のエスケープとoauthパラメータの並べ替えと結合は何回か使うのでメソッドにしました。

require 'openssl'
require 'uri'
require 'net/http'

# 文字列のエスケープ(: / = %をエスケープする。. _ -はそのまま)
def escape(value)
    URI.escape(value, Regexp.new("[^a-zA-Z0-9._-]"))
end

# oauth_headerの情報をアルファベット順に並べ替え & で結合
def sort_and_concat(oauth_header)
    oauth_header_array = oauth_header.sort
    param = ""
    oauth_header_array.each do |params|
        for i in 1..params.length
            param += params[i-1]
            if i % params.length  == 0
                param += "&"
            else
                param += "="
            end
        end
    end
    param = param.slice(0, param.length-1)
end

# リクエストトークン取得用のURL
request_token_url = "http://twitter.com/oauth/request_token"

# Twitterで登録したらもらえる
consumer_key = "XXXXXXXXXXXXXXXXXXXXXX"
consumer_secret = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"

# oauthパラメータたち
oauth_header = {
    # Consumer Key
    "oauth_consumer_key" => consumer_key,
    # 一意な値(今回は適当に実装)
    "oauth_nonce" => "AAAAAAAA",
    # 署名方式(HMAC-SHA1)
    "oauth_signature_method" => "HMAC-SHA1",
    # リクエスト生成時のタイムスタンプ(ミリ秒)
    "oauth_timestamp" => Time.now.to_i.to_s,
    # バージョン(1.0)
    "oauth_version" => "1.0",
}

# signature作成
# oauth_headerのパラメータをソートして連結
param = sort_and_concat(oauth_header)

# メソッドとURLとパラメータを&で連結する
value = "GET" + "&" + escape(request_token_url) + "&" + escape(param)

# sigunature_keyの作成
# リクエストトークン時は"CONSUMER_SECRET&"(アンドが入っている)
signature_key = consumer_secret + "&"

# hmac_sha1
sha1 = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, signature_key, value)
# base64エンコード signatureを生成できたので、これをoauth_signatureとする
oauth_header["oauth_signature"] = [sha1].pack('m').gsub(/\n/, '')

# GETする
uri = URI.parse(request_token_url)
proxy_class = Net::HTTP::Proxy(ARGV[0], 8080)
http = proxy_class.new(uri.host)
http.start do |http|
    # oauth_headerのパラメータをソートして連結
    param = sort_and_concat(oauth_header)

    res = http.get(uri.path + "?#{param}")

    if res.code == "200" then
        print "#{res.code}\n"
        print "#{res.body}\n"
    else
        print "ERROR: #{res.code}\n"
    end
end

結果はこんな感じで。

[f:id:kk_Ataka:20101121013615p:image]

成功するとbodyにoauth_token, oauth_token_secret他がくっついた値が帰ってきます。次はこれを使ってアクセストークンをもらいます!

まだRubyも知らない事多すぎる!

moridai

oauthの勉強の参考になりました。
ちなみに “sort_and_concat” の関数の中は
oauth_header.sort.map{|i|i.join(“=”)}.join(”&“)
と書くとすっきりかけると思います(動くはず…

1297579290

kk_Ataka

おお、ありがとうございます!
mapちょっと試してみます!

1299062371

関連記事(この記事の初版より古い記事はリンクがグレーで表示されます)

  1. 2010/12/07 [Ruby] [Twitter] [API] RubyでTwitterのタイムラインを取得してみる
  2. 2010/11/30 [Ruby] [Twitter] [API] RubyでTwitterのOAuth認証をしてみる その2
  3. 2011/01/23 [Ruby] [Twitter] [API] RubyでTwitterにツイートをキメてみる
  4. 2012/05/21 [SlideShare] [Heroku] [Ruby] [API] SlideShareのAPIを叩いてスライドをDLするRubyスクリプトをHerokuにデプロイした
  5. 2011/12/27 [Evernote] [Ruby] [API] EvernoteのAPIをRubyから叩きたい
  6. 2011/03/08 [Ruby] [API] RubyではてなのWSSE認証をしてはてブにブクマをポストする
  7. 2011/03/02 [Ruby] [API] Read it LaterをRubyで取得する
  8. 2017/03/30 [Ruby] rbenvを最新にして新しいバージョンのRubyをインストールできるようにする
  9. 2016/08/31 [Ruby] Rubyの変数DATABASE_URLでハマった話
  10. 2016/06/05 [Ruby] Rubyで自然言語っぽくcrontab管理できるwheneverを使う
  11. 2016/02/17 [Ruby] [Mac] El Capitan上に古いpumaをインストールすると失敗する
  12. 2015/02/28 [Ruby] rubyXLを使ってExcelを操作したい
  13. 2014/08/28 [Sphinx] [Ruby] [イベント] kawasaki.rb #015 でSphinx導入事例の発表をしてきました #kwskrb #sphinxjp
  14. 2014/07/13 [Sphinx] [Ruby] [イベント] kawasaki.rb #013 でSphinx導入事例の発表をしてきました と #011 #012 参加記録 #kwskrb #sphinxjp
  15. 2014/04/01 [Ruby] [Jekyll] [イベント] kawasaki.rb #010 で発表してきました #kwskrb