RubyでTwitterのOAuth認証をしてみる
随分前にTwitterがベーシック認証からOAuth認証に切り替えたという事で。Java+Wicket+AppEngineでベーシック認証を駆使して作っていたTwitterサイトが見れなくなったもんで切り替えました。(Wicketで始めるオブジェクト指向ウェブ開発|gihyo.jp … 技術評論社 を見つつ)
一応出来たんですが、結局のところOAuthがどうなってるのかよくわからなかった……ので、ちょっと一から書いてみようと。
id:Yoshiori さんのやる夫と Python で学ぶ Twitter の OAuth - YoshioriのBlog と、 id:yuroyoro さんのOAuthプロトコルの中身をざっくり解説してみるよ - ( ꒪⌓꒪) ゆるよろ日記がとっても詳しかったので、参考にしました。
大きな流れとしては、
- consumer_keyとconsumer_secretを発行してもらう
- リクエストトークンを発行してもらう
- アクセストークンを発行してもらう
の3項目。今回はリクエストトークンを発行してもらうところまでやります。
準備 Twitterにアプリを登録し、consumer_keyとconsumer_secretを発行してもらう
- Twitterにログイン > 設定 > 連携アプリ を選択
- 開発者の方へ > こちら を選択
- ページ下部の新しいアプリケーションを追加 を選択
後で編集もできるのでとりあえず入力しておく。下記の2項目はとりあえず
-
- アプリケーションの種類: クライアントアプリケーション
- 標準のアクセスタイプ: Read & Write
にしておく。
- 登録したら、consumer_keyとconsumer_secretをもらえるので控えておく
フォローをリクエストしました。の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つの値を&で連結する←この&はエスケープしない)
-
- http_methodの種類("GET"か"POST"。今回は"GET")
- "http://twitter.com/oauth/request_token"をエスケープしたもの
- 上記のパラメータからoauth_signatureを抜かしたパラメータをアルファベット順に並べてxxx=yyy&vvv=zzz……の形で連結した値をエスケープしたもの
- 署名キーを生成する
-
- リクエストトークンを発行してもらうときは"consumer_secret&"(consumer_secretのおしりに&を連結する)
……認証に失敗したとき、どこのステップで間違ってるのかわからなかったので非常に苦労しました。幸いsignatureを生成してくれるページ OAuth Signature生成サンプル があるので、ここで作成した値と同じ状況を作って比較しました。
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
結果はこんな感じで。
成功するとbodyにoauth_token, oauth_token_secret他がくっついた値が帰ってきます。次はこれを使ってアクセストークンをもらいます!
まだRubyも知らない事多すぎる!