まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2010/08/18(水)Pure PerlでのOAuth実装メモ

adiaryに、twitter投稿のためのOAuthを実装したときのメモです。PurePerl動作。

OAuthモジュール(OAuth::Lite::Consumer)を使わずに実装したので、同じことをやりたい人には参考になるかと思います。*1

*1 : PurePerlでなければNet::Twitterモジュールを利用するのが楽です。

資料とか

OAuthの流れ

一番最初に読むべきはRFC5849の元Draftの1.2例の項目です。とりあえず全体の流れが分からないと実装に手こずります。

twitterを前提として話をすると次のようになります。

  1. http://twitter.com/oauth_clientsにアクセスし、これから製作するアプリケーションを登録する。
  2. これにより「Consumer key(クライアント識別子)」および「Consumer secret(クライアント秘密鍵)」を得ます。
  3. クライアントソフトはユーザーからの要求があったとき、http://twitter.com/oauth/request_tokenへアクセスし、リクエストトークン(テンポラリクレデンシャル)及びリクエスト秘密鍵を得ます。
  4. クライアントソフトはユーザーに対しhttp://twitter.com/oauth/authorize?oauth_token=(リクエストトークン)へのアクセスを促し(もしくはリダイレクトをして)、アプリケーションからアカウントへのアクセスを許可させます。
  5. このとき、ユーザーのブラウザには数桁の暗証番号(verifier)が表示され、これをアプリケーション側に入力させます。
  6. リクエストトークン(テンポラリクレデンシャル)を使用しverifierと共にアクセストークンと秘密鍵*2を得るためhttp://twitter.com/oauth/access_tokenに対しリクエストを送信します。
  7. リクエストトークンおよびVerifierの正当性が確認されると、アクセストークン(トークンクレデンシャル)およびその秘密鍵が返されます。

以後は、得られたアクセストークン(トークンクレデンシャル)と秘密鍵により、自在にアクセスが可能になります。

この手順では暗証番号をユーザーに入力させる必要がありましたが、callbackによりこの手順を自動化することもできます。その場合リクエストトークン(テンポラリクレデンシャル)の要求を送る際にcallback URLを含めて送信します。こうすると、ユーザーがtwitter上にリクエストを許可した段階でcallback URLが呼び出され自動的に暗証番号(Verifier)を得ることができます。*3

*2 : 実際にtwitterにアクセスするためにセッションIDと確認パスワードのようなもの

*3 : twitterではcallback URLをクライアントソフト(Consumer key)ごとに固定して登録する必要があり、配布型webアプリケーションであるadiaryでは採用できませんでした。

リクエストトークンの取得

アクセストークンを得るまでの仮のトークンです。「Consumer key」および「Consumer secret」が正しければ得ることができます。例えば次のようなHTTPリクエストを送ります。

POST /oauth/request_token HTTP/1.0
Host: twitter.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Authorization: OAuth realm="",
 oauth_consumer_key="fqBn4Wmq2x3KyZUjPWYeNA",
 oauth_nonce="5PGfGBKqzkprkqh4g8K",
 oauth_signature="O0dNknEXA1rpCR5fSRhk18gd9AI%3D",
 oauth_signature_method="HMAC-SHA1",
 oauth_timestamp="1200102857",
 oauth_version="1.0"
ヘッダの内容意味
realmユーザー名*4
oauth_consumer_keyクランアントソフトの識別子
oauth_nonceセッションごとにクライアントが任意生成するランダムな文字列
oauth_signatureリクエストの正当性を確認するための署名
oauth_signature_method署名の方法
oauth_timestamp現在の時刻(UTC)
oauth_versionOAuthのVersion(なくても大丈夫)

リクエストが正しく確認されれば、次のような返答があります。

HTTP/1.1 200 OK
Date: Wed, 18 Aug 2010 13:40:58 GMT
Server: hi
Status: 200 OK
Last-Modified: Wed, 18 Aug 2010 13:40:58 GMT
X-Runtime: 0.01619
Content-Type: text/html; charset=utf-8
Content-Length: 145
Pragma: no-cache
X-Revision: DEV
Connection: close

oauth_token=aVxZsxVqtUA6PIZs6g442wlRE1IC4X8dZ4Cckd8NpM8&
oauth_token_secret=QYxVG7U9ISXpxBWibVOgtgbh0SZel0Op1Z3wt79I&
oauth_callback_confirmed=true ※実際には改行なしの1行

リクエストトークンでは成功時に必ず「oauth_callback_confirmed=true」が付いています。このときの oauth_token および oauth_token_secret がそれぞれリクエストトークンと秘密鍵になります。

*4 : 規格上、拡張として定義することができますが、twitterでは使われていません

HMAC-SHA1署名

クライアントソフトがtwitterに対してリクエストを送る際、HMAC-SHA1という署名が必要になります。OAuthにはいくつかの署名方法がありますが、HMAC-SHA1が現在のところ一般的なようです。署名はbase64エンコードする必要があります。

実装例はこの記事を参照。中身は以下のように利用出来る署名ルーチンです。

my $sig = &hmac_sha1($key, $msg);

このときの$keyと$msgについて解説します。

$keyは署名用の秘密鍵で consumer_secret と token_secret を & で連結したものです。

例えば「consumer_secret=AAA」「token_secret=BBB」ならば

$key = "AAA&BBB";

となります。もし、リクエストトークン要求のようにその時点でtoken_secretがない場合は、

$key = "AAA&";

となります。"&"が付いていることに注意してください。

署名するメッセージの生成

厄介なのはメッセージの方です。

  1. HTTPのメソッド(GET もしくは POST)
  2. リクエストするパス(/oauth/request_token 等)
  3. Authorizationの署名およびrealmを除く中身を連結した文字列

のそれぞれを & で連結したものが署名になります。

特に厄介なのは3です。先程のリクエストトークン要求を例に説明します。

Authorization: OAuth realm="",
 oauth_consumer_key="fqBn4Wmq2x3KyZUjPWYeNA",
 oauth_nonce="5PGfGBKqzkprkqh4g8K",
 oauth_signature="YLR5D8gkmPc5KxDuspxiWoibUd8%3D",
 oauth_signature_method="HMAC-SHA1",
 oauth_timestamp="1200102857",
 oauth_version="1.0"

この場合のAuthorizationの連結文字列は次のようになります。

oauth_consumer_key=fqBn4Wmq2x3KyZUjPWYeNA&oauth_nonce=5PGfGBKqzkprkqh4g8K&
oauth_signature_method=HMAC-SHA1&oauth_timestamp=1200102857&oauth_version=1.0
※実際には改行なしの1行

oauth_xxxは必ずアルファベット順に並べます(realmおよびoauth_signatureを除く全てを列挙する)。*5

1~3の文字列が生成できたら、それぞれをURIエンコードしてから & で連結します。間違いがないようにコードで書いておきます。

my $msg1 = 'POST';
my $msg2 = 'http://twitter.com/oauth/request_token';
my $msg3 = "oauth_consumer_key=fqBn4Wmq2x3KyZUjPWYeNA"
  . "&oauth_nonce=5PGfGBKqzkprkqh4g8K"
  . "&oauth_signature_method=HMAC-SHA1"
  . "&oauth_timestamp=1200102857"
  . "&oauth_version=1.0";

&uri_encode($msg1, $msg2, $msg3);
my $msg = "$msg1&$msg2&$msg3";
my $sig = &hmac_sha1("consumer_secret&", $msg); # http://adiary.blog.abk.nu/0274

sub uri_encode() {
	foreach(@_) {
		$_ =~ s|([^\w\-\.\~])|
			my $x = '%' . unpack('H2', $1);
			$x =~ tr/a-f/A-F/;	#重要!!
			$x;
		|eg;
	}
}

URIエンコードするとき除外する文字列に気をつけてください。また、URIエンコード後の文字は必ず大文字にすることに注意してください。

このルーチンから次の文字列が得られます。

【署名する文字列/$msg】POST&http%3A%2F%2Ftwitter.com%2Foauth%2Frequest_token
&oauth_consumer_key%3DfqBn4Wmq2x3KyZUjPWYeNA%26oauth_nonce%3D5PGfGBKqzkprkqh4g8K
%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1200102857
%26oauth_version%3D1.0
【署名文字列】YLR5D8gkmPc5KxDuspxiWoibUd8=

こうして得られた署名を oauth_signature として Authorization ヘッダに付けるのですが、署名を1度URIエンコードするのを忘れないようにしてください*6

...,oauth_signature="YLR5D8gkmPc5KxDuspxiWoibUd8%3D",...

callbackを得る場合

  • Authorization に oauth_callback="(コールバックURLをURIエンコードしたもの)" を含めます。
  • 署名生成時に「oauth_callback=(コールバックURLをURIエンコードしたもの)」を含めます。

*5 : Authorization に並べるときは要素名でソートする必要はありませんが、署名生成時は必ずアルファベット順に並べます。

*6 : "="と"+"で問題になります。

アクセス許可の取得

ユーザー(UA=ブラウザ)に対して、http://twitter.com/oauth/authorize?oauth_token=(リクエストトークン)へのアクセスを促します。もしくはリダイレクトしても構いません。

この処理では署名は一切不要です

ユーザーに表示文字列を入力させるか、コールバックを得る方法でverifier文字列を得ます。twitterの場合7桁の数字です。

(コールバックを受ける場合の例)
http://callback.example.com/callback?
oauth_token=hh5s93j4hdidpola&oauth_verifier=1234567

アクセストークンの取得

twitter APIにアクセスするために必要なアクセストークンを取得します。

POST /oauth/access_token HTTP/1.0
Host: twitter.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Authorization: OAuth realm="",
 oauth_consumer_key="fqBn4Wmq2x3KyZUjPWYeNA",
 oauth_nonce="qSXhhvyBb4C7jmPqGScD",
 oauth_signature="azAUM6sWZLj91pPVjsWhLdN8Cxc%3D",
 oauth_signature_method="HMAC-SHA1",
 oauth_timestamp="1210102857",
 oauth_token="aVxZsxVqtUA6PIZs6g442wlRE1IC4X8dZ4Cckd8NpM8",
 oauth_verifier="8102799",
 oauth_version="1.0"

oauth_verifier="8102799"が増えています。署名は次のように生成します。

【リクエスト先】http://twitter.com/oauth/access_token
【署名鍵】fqBn4Wmq2x3KyZUjPWYeNA&QYxVG7U9ISXpxBWibVOgtgbh0SZel0Op1Z3wt79I
 ※consumer_key&リクエストトークンkey
【署名する文字列】(1)(2)(3)をURIエンコードして&連結したもの
(1)POST
(2)http://twitter.com/oauth/access_token
(3)oauth_consumer_key=fqBn4Wmq2x3KyZUjPWYeNA
  &oauth_nonce=qSXhhvyBb4C7jmPqGScD
  &oauth_signature_method=HMAC-SHA1
  &oauth_timestamp=1210102857
  &oauth_token=aVxZsxVqtUA6PIZs6g442wlRE1IC4X8dZ4Cckd8NpM8
  &oauth_verifier=8102799
  &oauth_version=1.0  ※実際には改行なしの1行

レスポンス。

HTTP/1.1 200 OK
Date: Wed, 18 Aug 2010 16:22:13 GMT
Server: hi
Status: 200 OK
Last-Modified: Wed, 18 Aug 2010 16:22:12 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 164
X-Revision: DEV
X-Frame-Options: deny
Vary: Accept-Encoding
Connection: close

oauth_token=1234567xx-L1tWgd2WwjBiD4WUWDCix1MqFuQDFIDiG7vRGA50&
oauth_token_secret=ef2f74Da01XfNczBZy57jnWzbr6vX2H1BZ22JQf3iuQ&
user_id=1234567xx&screen_name=nabe_abk  ※実際には改行なしの1行

アクセストークンを得れば、リクエストトークン(テンポラリクレデンシャル)およびその秘密鍵は不要になります。

screen_nameは表示名ではなく、いわゆるtwitter上のユーザーIDに相当します。


twitter APIへのアクセス(TLへの発言)

twitter API日本語訳を参考に、TLの発言の例だけ示します。

  • http://twitter.com/statuses/update.xml
  • http://twitter.com/statuses/update.json

twitter側の仕様変更によりURLが変更になっています(2012/10)。

  • http(s)://api.twitter.com/1/statuses/update.xml
  • http(s)://api.twitter.com/1/statuses/update.json

結果を受け取る形式により「.xml」もしくは「.json」に対しリクエストを送ります。

発現データはフォーム形式で「status=(発言本文)」として送信します。

POST /1/statuses/update.xml HTTP/1.0
Host: api.twitter.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
Authorization: OAuth realm="",
 oauth_consumer_key="fqBn4Wmq2x3KyZUjPWYeNA",
 oauth_nonce="WER546dWkjfasloE",
 oauth_signature="(シグネチャ)",
 oauth_signature_method="HMAC-SHA1",
 oauth_timestamp="1220102857M",
 oauth_token="(アクセストークン)",
 oauth_version="1.0"

status=test+tweet

署名するデータ。

【署名鍵】consumer_key&アクセストークンkey(秘密鍵)
(1)POST
(2)http://api.twitter.com/1/statuses/update.xml
(3)oauth_consumer_key=fqBn4Wmq2x3KyZUjPWYeNA
  &oauth_nonce=WER546dWkjfasloE
  &oauth_signature_method=HMAC-SHA1
  &oauth_timestamp=1210102857
  &oauth_token=(アクセストークン)
  &oauth_version=1.0
  &status=test%20tweet  ※実際には改行なしの1行

うまく動作すれば、twitterに発言することができます。

twitterにupdateなどのデータを送る際フォームデータ(この例ではstatus)とAuthorizationヘッダの中身の両方の要素をアルファベット順に並べて署名対象メッセージを生成する必要があります。*7

フォーム要素のデータは当然ながらURIエンコードする必要があり、署名用メッセージはURIエンコードされた文字列を使用します。また日本語を使用する場合は文字コードとしてUTF-8を使用してください。

フォーム要素のURIエンコード

sub uri_encode_com {
	foreach(@_) {
		$_ =~ s|([^\w!\(\)\*\-\.\~\/:])|
			my $x = '%' . unpack('H2', $1);
			$x =~ tr/a-f/A-F/;
			$x;
		|eg;
	}
}

スペースを「+」に置換すると失敗します。また「:」を含むことに注意してください。

一方でhttpで送る時のエンコード(postデータ)は「%2f」のように小文字でも良く、POSTされたデータを一度元に復元してから再度署名確認していると思われます。

*7 : 一部のフォームデータは署名対象になりません。APIの解説を参照してください。

ツッコミ

ツッコミがあればコメント/TBしてください。記事は随時修正する予定です。