at posts/single.html

Sinatra と OAuth を使って Twitter のタイムラインを取得してみた

Sinatra で何か作ってみようと思い、 OAuth を使って Twitter のタイムラインを取得するサンプルを作った。 いろいろと試行錯誤していたんだけど、最終的には Twitterの地名なうbotを全blockするOAuthアプリのコードがとても参考になった。

事前準備: ライブラリのインストール

事前準備: Twitter への OAuth コンシューマ登録(APIキーの取得)

Applications Using Twitterのページで Register a new application から新規にアプリを登録する。

  • Callback URL は適当に設定。
    • 今のOAuthのバージョン (1.0a) では要求時にコールバックURLを指定するので、この設定値は使われないみたい。ここで結構はまった。
  • Application Type は Browser を選択
  • Timeline の取得だけなので、 Default Access type は Read-only を選択。万が一、キーが漏れても読み取り専用なので安心。
  • Use Twitter for login のチェックボックスはとりあえずオフにした。便利そうなのであとで調べる。

登録後に Consumer key と Consumer secret が発行される。

Twitter OAuth (1)

ちなみに、このスクリーンショットでのキーはちゃんと再発行している。

Sinatra アプリの作成

oauth-sample.rb というファイル名でアプリを作成。 Rails のようなジェネレータを使わずに、いきなり書き出せるところが Sinatra さんの魅力。 ソースコードは gist に載せた。この日記の最後にも貼り付けてる。

以下、ポイントだけ抜粋。

トップページの作成

トップページはログインのリンクを貼り付けているだけ。

get '/' do
  erb %{ <a href="/request_token">OAuth Login</a> }
end

OAuth で Twitter API を呼び出すまでのフロー

最近はWeb上にも Twitter + OAuth の情報が増えてきたので、簡単におさらい。

Twitter API を使ってタイムラインを取得するには、2つの方法がある。 1つがIDとパスワードでBasic認証する方法で、もう1つが OAuth を使う方法。 Web アプリなので、ユーザにTwitterのIDとパスワードを入力してもらうのはありえないので、OAuthを使う。

Twitter API で OAuth を使う場合は、以下の流れになる。

  • Consumer (これから作るSinatraアプリ) が Service Provider (Twitter) からリクエストトークンを取得する。
  • Consumer はユーザにリクエストトークンを渡し、Twitterの承認画面へリダイレクトする。
  • ユーザはTwitterの承認画面で、Consumerに自分の権限を渡すかどうかを選択する。(例のALLOW画面)
  • Consumer は Service Provier にリクエストトークンを渡し、引き替えにアクセストークンを取得する。
  • Consumer はアクセストークンを使って、 Twitter API を操作する。

ちょっと複雑かもしれないけど、要は「IDとパスワードの替わりにアクセストークンを使うよ」、「リクエストトークンはアクセストークンへの引換券だよ」という感じ。 詳しくはゼロから学ぶOAuthの「何が行われていたのか?」に書かれているシーケンスなどが参考になる。

OAuth の認証要求 (リクエストトークンの取得と Twitter へのリダイレクト)

まずは、 Twitter からリクエストトークンを取得し、ユーザを Twitter の承認画面へリダイレクトさせるまで。

OAuth::Consumer は gem でインストールした OAuth のライブラリ。 KEY と SECRET は、先ほど Twitter に登録した Consumer key と Consumer secret を設定しておく。

def oauth_consumer
  OAuth::Consumer.new(KEY, SECRET, :site => "http://twitter.com")
end

OAuth::Consumer の get_request_token メソッドを呼び出すと、 Twitter からリクエストトークンを取得する。 取得したリクエストトークンは OAuth::RequestToken オブジェクトとして返される。 OAuth::RequestTokenオブジェクトのauthorize_url メソッドで Twitter 承認画面の URL が取得できるので、 Sinatra の redirect メソッドで承認画面へとリダイレクトさせる。

get '/request_token' do
  callback_url = "#{base_url}/access_token"
  request_token = oauth_consumer.get_request_token(:oauth_callback => callback_url)
  session[:request_token] = request_token.token
  session[:request_token_secret] = request_token.secret
  redirect request_token.authorize_url
end

ここの注意点は、 get_request_token メソッドの引数として、コールバック先のURL (:oauth_callback) を指定すること。 ユーザが Twitter の承認画面で「ALLOW」をクリックすると、ここで指定したコールバック URL (/access_token) に戻ってくる。 もし、コールバックURLを指定しなかったら、ユーザの承認後に「You've successfully granted access to <アプリ名>」「下記の暗証番号を入力してください。」というメッセージとともに、以下のような画面が表示される。

Twitter OAuth (2)

なんどやってもこの数字の画面が表示されるので2日間くらい悩んでいたら、 Twitter で id:nashiki さんが oauth_callback required for web apps, since oauth gem by default forse PIN-based flow という情報を教えてくれた。 OAuth の仕様が 1.0 から 1.0a に変わったときに、コールバックURLの指定方法も変わったみたい。 そして、この数字 (PINコード) の画面はコールバックが使えないような環境 (デスクトップやモバイル) で使われるものみたい。 なるほど。

それから、リクエストトークンはユーザが Twitter の承認画面から戻ってきたあとにも必要になる(アクセストークンとの引き替えに使う)ので、ユーザのセッションに格納しておく。 Sinatra さんのセッションは文字列しか保存できない(OAuth::RequestToken オブジェクトを格納できない) ので、 request_token をそのまま保存せずに .token と .secret (いずれも文字列) を保存している。 (追記: Sinatra のセッションは Ruby のオブジェクトを Marshal + Base64 エンコードしているという記述もあるので、これは誤りかも。)

Twitter

Webブラウザには Twitter の承認画面が表示される。 アプリを登録したときに、Default Access type を Read-only としているので、「access and update」ではなく「access」となっている。 ここで ALLOW をクリック。

やっぱりこの画面は少し不親切だなぁ…。 コールバック先のドメインくらいは、画面に出してほしい。

Twitter OAuth (3)

リクエストトークンと引き替えにアクセストークンを取得する

Twitter の承認画面で ALLOW ボタンをクリックすると、コールバック先として登録していた URL に戻ってくる。 まずは、セッションに保存していたリクエストトークンを復元する。

get '/access_token' do
  request_token = OAuth::RequestToken.new(
    oauth_consumer, session[:request_token], session[:request_token_secret])

OAuth::RequestToken オブジェクトの get_access_token メソッドを呼び出す。 引数には、クエリストリングの oauth_token と oauth_verifier を指定する。 戻り値は、OAuth::AccessToken オブジェクトになる。 get_access_token メソッドを呼び出すと、 Twitter と通信してリクエストトークンと引き替えにアクセストークンを取得する。 リクエストトークンからアクセストークンの取得は1回のみ可能。 同じリクエストトークンで2回アクセストークンを取得しようとすると、 Twitter はステータス 401 を返し、 OAuth::Unauthorized 例外が投げられる。

  begin
    @access_token = request_token.get_access_token(
      {},
      :oauth_token => params[:oauth_token],
      :oauth_verifier => params[:oauth_verifier])
  rescue OAuth::Unauthorized => @exception
    return erb %{ oauth failed: <%=h @exception.message %> }
  end

取得したアクセストークンは、 Twitter API を呼び出すときに必要になるので保存しておく。 ここではユーザのセッションに格納しているけど、ちゃんとしたアプリを作る場合は DB などで永続化する必要がある。 また、 Sinatra さんのセッションは「内容が読めるけど改ざんはできない」という MAC 付きの Cookie ベースなので、ここにアクセストークンの secret を入れていいのかという疑問もある。 まぁ、 Consumer のシークレットキーじゃないし、承認した本人しか見られないので実害はあまりないだろうけど。

  session[:access_token] = @access_token.token
  session[:access_token_secret] = @access_token.secret

サンプルなので、認証が終わったよというメッセージとアクセストークンの内容を画面に表示している。 実際には redirect メソッドを使って次の画面にリダイレクトしてOK。

Sinatra さんは erb と書くだけで erb テンプレートを呼び出せるので楽。

  erb %{
    oauth success!
    <dl>
      <dt>access token</dt>
      <dd><%=h @access_token.token %></dd>
      <dt>secret</dt>
      <dd><%=h @access_token.secret %></dd>
    </dl>
    <a href="/timeline">go timeline</a>
  }
end

アクセストークンを使ってタイムラインを取得

取得したアクセストークンを使って、 Twitter のタイムラインを取得する。 ここまでは OAuth ライブラリの仕事だったけど、ここからは Twitter ライブラリのお仕事。 複数の画面で Twitter API を呼び出すことを考慮して、 Sinatra さんの before メソッド内で Twitter ライブラリを初期化している。

まず、 consumer key と secret から Twitter::OAuth オブジェクトを生成する。 次にセッションに保存していたアクセストークンを、 authorize_from_access メソッドで Twitter::OAuth オブジェクトに設定する。 最後に、 Twitter::OAuth オブジェクトを Twitter::Base オブジェクトに渡す。

before do
  if session[:access_token]
    twitter_oauth = Twitter::OAuth.new(KEY, SECRET)
    twitter_oauth.authorize_from_access(
      session[:access_token], session[:access_token_secret])
    @twitter = Twitter::Base.new(twitter_oauth)
  else
    @twitter = nil
  end
end

長かったけど、これで OAuth を使って Twitter API を呼び出せるようになった。

get '/timeline' do
  redirect '/' unless @twitter
  erb %{
    <dl>
    <% @twitter.friends_timeline.each do |twit| %>
      <dt><%= twit.user.name %></dt>
      <dd><%= twit.text %></dd>
    <% end %>
    </dl>
  }
end

あれ、 Sinatra さんの解説をしようと思ったのに、 OAuth の説明になっちゃった。

Sinatra のセッション管理

Sinatra のセッションは、以下のように指定すると有効になる。

set :sessions, true
enable :sessions

このセッションは、単純にデータを Cookie に入れているだけ。 なので、簡単にクライアント側で改ざんできてしまう(えー)。

代わりに、SinatraのSessionには注意 - GIOの日記で指定されているように secret キーを指定する必要がある。 Sinatra のセッションは Rack ベースなので、使い方は Rack::Session::Cookie を参照。 ただし、引数の :secret の値 'change_me' をそのまま使っちゃダメ。 この値が漏れると、 Cookie も改ざんできてしまう。 secret に単純な文字列を使うことは、 Web アプリのセキュリティホールに繋がる。

僕は以下のように、ランダムな数字のハッシュ値を、Cookieのキーに設定するようにした。

use Rack::Session::Cookie, :secret => Digest::SHA1.hexdigest(rand.to_s)

Ruby1.8.7以降なら、 rand と hexdigest の代わりに、安全な乱数発生器である SecureRandom.hex を使った方がいいと思う。

use Rack::Session::Cookie, :secret => SecureRandom.hex(32)

いずれも、サーバの起動時に生成しているので、サーバを再起動するとそれまでのセッションが無効になる。 なので、毎回プロセスが起動される CGI 環境では、この方法は使えない。

ちなみに、 Java や PHP や cgi.rb とは違って、セッションに格納した値が Cookie に入るから、ユーザに知られては困るような情報をセッションに入れちゃダメ。 Rails 使っている人にはおなじみの話だけれども。

Sinatra さん(というか Rack)のセッション回りは改めて考える必要がありそうだなー。

サービスの起動

Sinatra で作成したアプリを起動するには、以下のように ruby から呼び出すだけでいい

$ ruby oauth-sample.rb
== Sinatra/0.9.4 has taken the stage on 4567 for development with backup from Thin
>> Thin web server (v1.2.2 codename I Find Your Lack of Sauce Disturbing)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:4567, CTRL+C to stop

Thin という Ruby で書かれた Web サーバ上のサービスとして起動されている。 Thin の場合、ポート番号は 4567 が使われる。 あとは、 80番で起動している Apache からプロキシさせるとか、使い方はいろいろ。

ただし、 ruby コマンドで起動した場合は、ソースを書き換えるたびにサーバの再起動が必要。 自動リロードを実現するには、 ruby コマンドではなく shotgun コマンドを使う。 (コマンドが入っていないときは gem install shotgun で)

$ shotgun oauth-sample.rb
== Shotgun starting Rack::Handler::Mongrel on localhost:9393

なぜか shotgun の場合は Thin ではなく Mongrel がポート 9393 で起動している。 shotgun を使うと、ソースを書き換えるとリロード無しでサービスに反映される。 開発時は shotgun で、本番時は Apache + mod_proxy + Thin/Mongrel や Apache + Passenger がいいかも。

情報源

ソースコード

関連する日記