OAuth認証を通過してTwitterの自分のアカウントのTLを取得するプログラムをPerlで書いてみた。
色々間違っていそうだけど、とりあえずTLを取得できたので、その流れをメモ。
Net::OAuthとか使うんだろwwwwとか思ってた人、ごめんなさい。
ほぼ標準で入っているだろうと思われるモジュールでLWPとDigestしか使いません。
つまり、OAuth認証を自前で実装します。
まだ欠点はあって、認証コードの入力は手動です。(自動化するとなると、アカウントとパスワード入力が必要になる)
参考
OAuthプロトコルの中身をざっくり解説してみるよ - ( ꒪⌓꒪) ゆるよろ日記
OAuth Core 1.0a
http://tzmtk.pbworks.com/w/page/7618697/OAuthCore10JP
(アプリケーションを登録)
Twitter / アプリケーション登録
http://twitter.com/apps/new
適当に登録を済ませる。登録すると、5つの情報が得られます。
- Consumer key : キーです
- Consumer secret : 外部に見せてはまずい情報です。
- フォローをリクエストしました。:なぜ訳したし。リクエストトークン(oauth_token)を得るためのURL
- Access token URL : リクエストトークンを使ってアクセストークンを得るためのURL
- Authorize URL : アクセストークンを得るときに認証をする際に使うURL
ConsumerkeyとConsumerSecretはOAuth認証で使います。
後の3つは後の工程でリクエストを発行する時に必要です。
リクエストトークンを要求・受信
oauth/request_tokenにリクエストを発行します。
その時にAuthorizationヘッダーに以下のパラメータ情報をセットします。
'oauth_consumer_key' => アプリケーション固有のキー
'oauth_nonce' => リクエスト毎に一意な文字列
'oauth_signature' => 署名
'oauth_signature_method' => 署名の方式
'oauth_timestamp' => タイムスタンプ(UTC)
'oauth_version' => バージョン(今は'1.0')
'oauth_nonce'はリクエスト毎に一意な文字列であればなんでもいいようです。ただのタイムスタンプにすることも多いようです。
'oauth_signature'は、'oauth_signature_method'に沿って生成する必要があります。
TwitterのOAuth認証ではHMAC-SHA1という方式を使うので、これを使って'oauth_signature'を生成します。
oauth_signatureの作り方
HMAC-SHA1(key, msg)のダイジェストをBase64に変換する事で生成できます。
keyは"oauth_consumer_secret&oauth_token_secret"で生成できます。
'oauth_consumer_secret'はアプリケーション固有の隠し情報ですが、
'oauth_token_secret'はリクエストトークン生成時にはまだ知らない状態なので、ここでのkeyは"oauth_consumer_secret&"となります。
msgは、HTTPでリクエストを発行するときのメソッド(method)と、URL,パラメータ情報を&で連結したもの、をそれぞれパラメータエンコードし、さらに&で繋げたものです。
( 参考: http://oauth.net/core/1.0a/#encoding_parameters )
パラメータエンコード関数をPEとしたとき、msgは以下となります。
PE(method) '&' PE(URL) '&' PE(クエリ情報)
このkeyとmsgをHMAC-SHA1関数に渡してダイジェストを生成し、Base64に変換します。
Authorizationヘッダー
"OAuth a={a}, b={b}..."という形式で生成します。({a}は変数aを示します)
これをHTTPリクエストを発行する時にAuthorizationに付加して送信します。
認証コード(oauth_verifier)の取得
oauth/authorizeにoauth_token={oauth_token}というパラメータを付加してGETメソッドでリクエストするだけです。
ttp://twitter.com/oauth/authorize?oauth_token={oauth_token}
という形になります。
しかし、実際にはユーザの認証を行うので、IDとパスワードを入力する必要があります。
アプリケーションにパスワードを入力するのはちょっと・・・と思う人はいるはずなので、
ここはユーザーさんにURLだけを表示してブラウザでアクセスしてもらって、認証コードを入手してきてもらいます。
ちなみにこのURLはワンタイム方式なので、1度認証が終わると使えなくなります。
アクセストークンを要求・受信
基本的にリクエストトークンと全く同じ手順を踏みます。
違う点はoauth/access_tokenに'POST'でリクエストを発行する事と、oauth_tokenにリクエストトークンで取得したoauth_tokenを、oauth_verifierに認証コードをセットしたパラメータに追加する点と、oauth_token_secretを使う点だけです。
oauth_signatureを作るときは、リクエストトークンで得たoauth_token_secretを与えて追加したパラメータ含めてsignatureを生成します。
リクエストに成功すると、やはりリクエストトークンと同じように、oauth_tokenとoauth_token_secretが帰ってきます。
これがアクセストークンになり、OAuth認証に通過した証明になるものになります。
アクセストークンの使い方
OAuth認証でやってきたのと同じようにAuthorizationヘッダーにアクセストークンを含めたパラメータを付加してAPIへリクエストを発行します。
結果はAPIの実装に依存した結果が返ります。
実装
PerlでTwitterのタイムラインを取得するコードを実装してみた。
エラーチェックが甘かったり、デバッグ用のprintfが沢山書かれていたり、コーディングが汚いけど、気にしない。
リクエストの発行はLWP::UserAgentに任せて、signatureの算出はSHA1の計算だけDigest::SHA1で得る。
アクセストークンを取得したら、oauth.datに記録して、2回目以降は認証作業を飛ばす。
標準出力をtl.xmlで受け取っている。
ファイルの構成としては、アプリケーションをNimonoとして、こんな感じ。
Nimono.pm
Nimono
└OAuth.pm
UseNimono.pl
(oauth.dat) OAuth認証データ
(tl.xml) タイムラインのデータ
Nimono/OAuth.pm
package Nimono::OAuth; use strict; use warnings; use Digest::SHA1; use LWP::UserAgent; my $RequestTokenPath = 'oauth/request_token'; my $AccessTokenPath = 'oauth/access_token'; my $AuthorizePath = 'oauth/authorize'; sub new{ my $class = shift; my $oauth_url = shift; my $consumer_key = shift; my $consumer_secret = shift; my $param = shift; $oauth_url =~ s/\/$//; my $self = { useragent => new LWP::UserAgent(agent=>'OAuth Activater', timeout=>60), oauth_url => $oauth_url, oauth_consumer_key => $consumer_key, # コンシューマーキー oauth_consumer_secret => $consumer_secret, # シークレット # request_token # oauth_consumer_key => $consumer_key, oauth_signature => '', # 署名 oauth_signature_method => 'HMAC-SHA1', # 署名方式 HMAC-SHA1, RSA-SHA1, and PLAINTEXT oauth_timestamp => '', # タイムスタンプ oauth_nonce => '', # ユニークな値 oauth_version => '1.0', # OAuthのバージョン oauth_signature => '', # 署名(取得するのでここでは定義しない) # request_token - response request_token => { oauth_token => '', oauth_token_secret => '', oauth_callback_confirmed => '', }, oauth_token => '', # アクセス/リクエストトークン oauth_token_secret => '', # アクセストークン・シークレット oauth_verifier => '', # PINコード }; if(!$self->{oauth_url} || !$self->{oauth_consumer_key} || !$self->{oauth_consumer_secret}){ die('need consumer_key and consumer_secret'); } $self = bless($self,$class); return $self; } #acceser sub oauth_token{ my $self = shift; return $self->{oauth_token}; } sub oauth_token_secret{ my $self = shift; return $self->{oauth_token_secret}; } sub oauth_consumer_secret{ my $self = shift; return $self->{oauth_consumer_secret}; } sub oauth_consumer_key{ my $self = shift; return $self->{oauth_consumer_key}; } # method sub init_param{ my $self = shift; $self->{oauth_timestamp} = time(); # タイムスタンプ $self->{oauth_nonce} = randomNonce(); # ユニークな値 } # ランダムなNonceの値を24〜32文字で返す sub randomNonce{ my @ch = ('a'..'z', '0'..'9', 'A'..'Z'); my $loop = 24+rand(8); my $nonce = ''; for(my $i=0; $i<$loop; ++$i){ $nonce .= $ch[rand(@ch)]; } return $nonce; } # リクエストトークンを得る # ret not:失敗 not以外:成功($self->{request_token}->{oauth_token}/{oauth_token_secret}に情報が入る) sub getRequestToken{ my $self = shift; my $request_token_url = "$self->{oauth_url}/$RequestTokenPath"; $self->init_param(); $self->{request_token}->{oauth_token} = $self->{request_token}->{oauth_token_secret} = ''; # generate oauth_signature $self->{oauth_signature} = $self->makeSignature('', 'GET', $request_token_url, ['oauth_consumer_key', 'oauth_nonce', 'oauth_signature_method', 'oauth_timestamp', 'oauth_version'] ); my $header = $self->makeAuthorizationHeader( ['oauth_consumer_key', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method', 'oauth_timestamp', 'oauth_version'] ); my $re = $self->{useragent}->get($request_token_url, Authorization => "$header", "Content-Type" => "application/x-www-form-urlencoded"); if($re->code() != 200){ # error print STDERR "getRequestToken error\n"; return 0; } foreach my $pair(split(/&/,$re->content)){ my($k,$v) = split(/=/,$pair,2); $self->{request_token}->{$k} = $v if exists $self->{request_token}->{$k}; } return ($self->{request_token}->{oauth_token} && $self->{request_token}->{oauth_token_secret}); } # アクセストークンを得る # ret not:失敗 not以外:成功($self->{oauth_token}/{oauth_token_secret}に情報が入る) sub getAccessToken{ my $self = shift; my $verifier = shift; # 暗証番号 if(not $self->{request_token}->{oauth_token} || not $self->{request_token}->{oauth_token_secret}){ print STDERR "getting request token...\n"; if($self->getRequestToken() == 0){ return 0; } } my $access_token_url = "$self->{oauth_url}/$AccessTokenPath"; $self->init_param(); $self->{oauth_verifier} = $verifier; $self->{oauth_token} = $self->{request_token}->{oauth_token}; $self->{oauth_token_secret} = ''; # generate oauth_signature $self->{oauth_signature} = $self->makeSignature($self->{request_token}->{oauth_token_secret}, 'POST', $access_token_url, ['oauth_consumer_key', 'oauth_nonce', 'oauth_signature_method', 'oauth_timestamp', 'oauth_version' ,'oauth_token', 'oauth_verifier'] ); # HTTP Header (Authorization) my $header = $self->makeAuthorizationHeader( ['oauth_consumer_key', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method', 'oauth_timestamp', 'oauth_version' ,'oauth_token', 'oauth_verifier'] ); # request my $re = $self->{useragent}->post($access_token_url, Authorization => "$header", "Content-Type" => "application/x-www-form-urlencoded"); if($re->code() != 200){ # error print STDERR "getAccessToken error\n"; return 0; } # parse foreach my $pair(split(/&/,$re->content)){ my($k,$v) = split(/=/,$pair,2); $self->{$k} = $v if exists $self->{$k}; } # print $re->as_string; return ($self->{oauth_token} && $self->{oauth_token_secret}); } sub makeOAuthHeader{ my $self = shift; my $url = shift; $self->init_param(); my @param = ('oauth_consumer_key', 'oauth_nonce', 'oauth_signature_method', 'oauth_timestamp', 'oauth_version' ,'oauth_token'); $self->{oauth_signature} = $self->makeSignature($self->{oauth_token_secret}, 'GET', $url, \@param); push @param, 'oauth_signature'; return $self->makeAuthorizationHeader(\@param); } # 情報から署名を生成 # 1. {oauth_consumer_key}&{oauth_token_secret}, method, URL, Query(param)列を渡す # 2. method, URL, Queryをurlエスケープしたものを&で結合 # 3. ハッシュ関数にHMACを適用したハッシュを生成(key, msg) # 4. urlエスケープして返す # SecretKey, Method, URL, Parameter sub makeSignature{ my $self = shift; my($key, $method, $url, $param) = @_; # 1 $key = $self->{oauth_consumer_secret}.'&'. $key; # {oauth_consumer_key}&{oauth_token_secret} my $msg = "$method". "&" . paramEncode($url) . "&" . paramEncode(join('&', map{"$_=$self->{$_}"} sort @$param)); # 2 if($self->{oauth_signature_method} eq 'HMAC-SHA1'){ # 3 return paramEncode( hmac_sha1_b64digest($key, $msg) ); }else{ die("$self->{oauth_consumer_secret} is not support."); } return paramEncode($msg); # 4 } # 認証ヘッダのAuthorization部分を生成 sub makeAuthorizationHeader{ my $self = shift; my $param = shift; return "OAuth realm=\"\"," . join ",", map {"$_=\"$self->{$_}\""} sort @$param; } # OAuth認証済みか否か {oauth_token} && {oauth_token_secret} があれば良い sub authorized{ my $self = shift; return ($self->{oauth_token} && $self->{oauth_token_secret}); } # 認証するためにURLを取得 sub authorizeUrl{ my $self = shift; # リクエストトークンを持っていない if(not $self->{request_token}->{oauth_token}){ print STDERR "getting request token...\n"; if( not $self->getRequestToken()){ return ''; } } my $authorize_url = "$self->{oauth_url}/$AuthorizePath"; $authorize_url .= "?oauth_token=$self->{request_token}->{oauth_token}"; return $authorize_url; } sub consoleAuthorize{ my $self = shift; # リクエストトークンを持っていない my $authorize_url = $self->authorizeUrl(); if(not $authorize_url){ print STDERR "consoleAuthorize Error\n"; return ''; } print STDERR "Access and get code.\nURL: $authorize_url\n"; print STDERR "input PIN code: "; my ($input) = <STDIN> =~ /(\d+)/; # my $re = $self->{useragent}->get($authorize_url); # my $re = $self->{useragent}->post($authorize_url); # print STDERR "=== authorize response ===\n" . $re->as_string() . "\n"; # print $re->content; return $input; } # return Base64 digest sub hmac_sha1_b64digest{ my ($key, $msg) = @_; # init my $object = new Digest::SHA1(); if(length($key) > 64){ $key = $object->add($key)->digest; $object->reset(); } # calc $msg = $object->add($key ^ "\x36" x 64, $msg)->digest; $object->reset(); # return return $object->add($key ^ "\x5C" x 64, $msg)->b64digest . "="; } sub paramEncode{ my $str = shift; $str =~ s{[^A-Za-z0-9\-\_\.\~]}{sprintf("%%%02X",unpack("C", $&))}eg; return $str; } sub outputOAuthFile{ my $self = shift; my $file = shift || return 0; open(my $FH, '>', $file) || return 0; print $FH "oauth_token=$self->{oauth_token}\n"; print $FH "oauth_token_secret=$self->{oauth_token_secret}\n"; return close($FH); } sub inputOAuthFile{ my $self = shift; my $file = shift || return 0; open(my $FH, '<', $file) || return 0; while(defined(my $line = <$FH>)){ chomp $line; my($k,$v) = split(/=/,$line,2); $self->{$k} = $v if exists $self->{$k}; } return close($FH); } 1;
Nimono.pm
CONSUMER_KEYとCONSUMER_SECRETはアプリケーション毎に自分で設定します。
package Nimono; use strict; use warnings; use Nimono::OAuth; sub new{ my $class = shift; my $CONSUMER_KEY = 'XXXXXXXXXXXXXXXXXXX'; my $CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; my $HOST = 'http://twitter.com/'; $HOST =~ s{/+$}{}; my $self = { host => $HOST, oauth => new Nimono::OAuth($HOST, $CONSUMER_KEY, $CONSUMER_SECRET), oauthfile => 'oauth.dat', timeline => [], }; $self->{oauth}->inputOAuthFile($self->{oauthfile}); return bless($self,$class); } sub authorize{ my $self = shift; if((not $self->{oauth}->authorized()) && (not $self->{oauth}->getAccessToken( $self->{oauth}->consoleAuthorize() ))){ return 0; } return 1; } sub request{ my $self = shift; my $method = (lc(shift) eq 'post') ? 'post' : 'get'; my $url = shift; my $header = $self->{oauth}->makeOAuthHeader($url); return $self->{oauth}->{useragent}->$method($url, Authorization => "$header", "Content-Type" => "application/x-www-form-urlencoded"); } sub getFriendTimeLine{ my $self = shift; my $path = 'statuses/friends_timeline.xml'; my $url = "$self->{host}/$path"; my $header = $self->{oauth}->makeOAuthHeader($url); my $re = $self->{oauth}->{useragent}->get($url, Authorization => "$header", "Content-Type" => "application/x-www-form-urlencoded"); return $re->content; } sub getShow{ my $self = shift; my %param = @_; my $path = "statuses/show/$param{id}.xml"; my $url = "$self->{host}/$path"; my $re = $self->request('GET', $url); return $re->content; } sub DESTROY{ my $self = shift; printf STDERR "----- \$self->{oauth} DUMP -----\n"; printf STDERR ("%-15s => %s\n", $_, $self->{oauth}->{$_}) foreach(sort keys %{$self->{oauth}}); $self->{oauth}->outputOAuthFile($self->{oauthfile}); } 1;
UseNimono.pl
use strict; use warnings; use Nimono; my $o = new Nimono(); if(not $o->authorize()){ die('認証失敗'); } print $o->getFriendTimeLine();
実行するとこんな感じ。
初回
>UseTwitter.pl > tl.xml
getting request token...
Access and get code.
URL: http://twitter.com/oauth/authorize?oauth_token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
input PIN code: [上のURLで得た認証コードを入力]
----- $self->{oauth} DUMP -----
# dump data #
2回目以降(oauth.datの情報が有効な間)
UseTwitter.pl > tl.xml
----- $self->{oauth} DUMP -----
# dump data #