2018年以降の記事はGitHub Pagesに移行しました

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

随分前にTwitterがベーシック認証からOAuth認証に切り替えたという事で。Java+Wicket+AppEngineでベーシック認証を駆使して作っていたTwitterサイトが見れなくなったもんで切り替えました。(Wicketで始めるオブジェクト指向ウェブ開発|gihyo.jp … 技術評論社 を見つつ)

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

id:Yoshiori さんのやる夫と Python で学ぶ Twitter の OAuth - YoshioriのBlog と、 id:yuroyoro さんのOAuthプロトコルの中身をざっくり解説してみるよ - ( ꒪⌓꒪) ゆるよろ日記がとっても詳しかったので、参考にしました。

大きな流れとしては、

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

の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つの値を&で連結する←この&はエスケープしない)
    1. http_methodの種類("GET"か"POST"。今回は"GET")
    2. "http://twitter.com/oauth/request_token"をエスケープしたもの
    3. 上記のパラメータからoauth_signatureを抜かしたパラメータアルファベット順に並べてxxx=yyy&vvv=zzz……の形で連結した値をエスケープしたもの
  • 署名キーを生成する
    1. リクエストトークンを発行してもらうときは"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も知らない事多すぎる!