RubyでNet::HTTPを使ってゴリゴリとTwitterのOAuth認証してみた

注:
最初OAuthライブラリの使い方を調べないままNet::HTTPを使って試してみたものです。
OAuthライブラリ使うだけでかなり手軽に書けます、ほんとに。

TwitterでのOAuth認証

OAuth認証の流れとしては

  1. アプリを登録しconsumer_key, consumer_secretを取得
  2. この2つを使い、リクエストトークン(oauth_token, oauth_token_secret)を取得
  3. ここまでに出てきた4つとPINコードを使い、アクセストークンを取得
  4. あとはTwitterライブラリを使うなり、自分でゴリゴリ書く

コード

https://gist.github.com/1385914

require 'openssl'
require 'uri'
require 'net/http'
require 'rubygems'
require 'twitter'


class OAuthAccessToken
  def initialize(consumer_key, consumer_secret)
    @request_token_uri = 'http://twitter.com/oauth/request_token'
    @access_token_uri = 'http://twitter.com/oauth/access_token'
    @authorize_uri = 'http://api.twitter.com/oauth/authorize'

    @consumer_secret = consumer_secret
    @token = ''
    @token_secret = ''

    # oauthのヘッダ                                                                                                                                                                  
    @header = {
      'oauth_consumer_key' => consumer_key,
      'oauth_nonce' => 'ABCDEFG',
      'oauth_signature_method' => 'HMAC-SHA1',
      'oauth_timestamp' => Time.now.to_i.to_s,
      'oauth_version' => '1.0'
    }

    create_signature(@request_token_uri)
    get_request_token
    get_pincode unless @token == ''
  end

  def get_access_token
    uri = URI.parse(@access_token_uri)
    http = Net::HTTP.new(uri.host)

    http.start do |h|
      create_signature(@access_token_uri, @token_secret)
      response = h.get("#{uri.path}?#{concat_header}")

      if response.code == '200'
	parse_response(response.body)
      else
        puts response.code

        puts response.body
        puts "error"
      end
    end
  end

  private
  def escape (value)
    URI.escape(value, /[^a-zA-Z0-9_\-\.]/)
  end

  # ヘッダの要素を連結
  def concat_header
    params = ''
    @header.each do |key,value|
      params += "#{key}=#{value}&"
    end
    params.chop
  end

  # シグニチャを作成
  def create_signature(token_uri, access_token_secret = '')
    # ヘッダのパラメータを連結して、HTTPメソッドとも連結
    params = concat_header
    value = "GET&#{escape(token_uri)}&#{escape(params)}"

    # signature keyの作成
    signature_key = "#{@consumer_secret}&#{access_token_secret}"

    # hmac_sha1
    sha1 = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, signature_key, value)
    @header['oauth_signature'] = [sha1].pack('m').gsub(/\n/, '')
  end

  def get_request_token
    uri = URI.parse(@request_token_uri)
    http = Net::HTTP.new(uri.host)

    http.start do |h|
      response = h.get("#{uri.path}?#{concat_header}")

      if response.code == '200'
        tokens = parse_response(response.body)

        @token = tokens['oauth_token']
        @token_secret = tokens['oauth_token_secret']
        @header['oauth_token'] = @token
      else
        p response.code
      end
    end
  end

  def get_pincode
    authorize_uri = "#{@authorize_uri}?oauth_token=#{@token}" 
    system('open', authorize_uri) || puts("Access here: #{authorize_uri}\nand...")
    
    print 'input PIN Code: '
    @header['oauth_verifier'] = $stdin.gets.strip.chomp
  end

  # レスポンスのボディをパース
  def parse_response(body)
    params = { }
    body.split('&').each do |param|
      token = param.split('=')
      params[token[0]] = token[1]
    end
    params
  end
end


if ARGV[0] && ARGV[1]
  consumer_key = ARGV[0]
  consumer_secret = ARGV[1]
else
  consumer_key = $stdin.gets.chomp
  consumer_secret = $stdin.gets.chomp
end

tokens = OAuthAccessToken.new(consumer_key, consumer_secret).get_access_token
if tokens.class == Hash
  Twitter.configure do |config|
    config.consumer_key = consumer_key
    config.consumer_secret = consumer_secret
    config.oauth_token = access_token
    config.oauth_token_secret = access_token_secret
   end
    
  Twitter.home_timeline.first.text
else
  puts "Error: cannot get 'access_token' and 'access_token_secret'"
end

コードについての忘備録

  • 主にやりたいことはTwitterライブラリにトークンを渡すことなので、アクセストークンを取り出すメソッドだけパブリックに
シグニチャ生成について
oauth_consumer_key Twitterで提供されるconsumer_key
oauth_nonce 一意な値
oauth_signature 認証のための暗号
oauth_signature_method 認証方式
oauth_timestamp タイムスタンプ(ミリ秒)
oauth_version バージョン(必須ではないが、付ける場合は"1.0")
oauth_token リクエストトークン
oauth_verifier PIN コード

これらはURLの後ろにつけるパラメータ

  • リクエストトークンを得る際にはoauth_token, oauth_verifierは要らない(というかまだ持ってない)
  • アクセストークンを得る際には全て使う

HTTPメソッド+URL+パラメータを連結した文字列を署名キーをもとに暗号化して、base64方式で暗号化

署名キーは以下の文字列

    • リクエストトークンではconsumer_secretのみ
    • アクセストークンではconsumer_secretとoauth_token_secretを&で連結したもの

引っかかったところ

  • accessをacessとtypo
    • 変数名はいいが、URLで間違えて302のレスポンスコードばかり
  • アクセストークンを得る際にURLをリクエストトークンのものにしていた
  • oauth_tokenをaccess_tokenと書いて早とちり

typoがひどいコードだった