速攻で作る OpenSocialアプリ ( RSA-SHA1 )

前回のブログにて OpenSocial Client Library Ruby版に付属している Gifts アプリを動作させるまで説明しました。

makeRequestの署名方式に RSA-SHA1 を用いる

OpenSocialコンテナ(Orkut) -> 外部サーバへのリクエストの署名方式には HMAC-SHA1 を用いていましたが、今回はより一般的な RSA-SHA1 を用いた方法を説明します。

HMAC-SHA1では consumer key と consumer secret の2つが必要でしたが、RSA-SHA1ではそれらは必要なく、公開鍵だけが必要となります。

■ OpenSocialコンテナ(Orkut) -> 外部サーバ のリクエストの署名方式を HMAC-SHA1 から RSA-SHA1 へ変更

GIFT_SAMPLE/public/gifts.xml の 14行目辺りのparams[“OAUTH_SERVICE_NAME”] = “HMAC”;
をコメントアウトしてください。
この変更により OpenSocialコンテナ(Orkut) -> 外部サーバへのリクエストの署名方式がデフォルトの RSA-SHA1 になります。

■ Rails側で RSA-SHA1 にて署名されたリクエストを受け取れるようにする

○ 公開鍵の取得

OpenSocialコンテナ(Orkut) -> 外部サーバへのリクエストのパラメータに
xoauth_signature_publickey=pub.1199819524.-1556113204990931254.cer
というkeyとvalueが付与されてきますが、この pub.1199819524.-1556113204990931254.cer ファイルが公開鍵となります。

で、そのファイルはどこにあるのかというと http://sandbox.orkut.com/46/o/pub.1199819524.-1556113204990931254.cer にあります。

xoauth_signature_publickeyの値が変更されたらどうするんだ?という疑問が浮かぶかもしれませんが、Orkutの場合は、xoauth_signature_publickeyの値が変わる時にはGoogleが前もって告知してくれますので、現状はこの公開鍵のファイルの内容をソースコードにハードコーディングしておいても大丈夫だと思います。
※逆に、自動で公開鍵ファイルを取得するような実装は危険です。そのファイルが正規のものかどうか判断のしようがありませんので。

○ gifts コントローラの修正

opensocial client library ( gem opensocial-0.0.2 )ですが、何故か署名方式に HMAC-SHA1 しか使用できないようになっています・・・。
RSA-SHA1の署名方式が使用できるようにします。

gifts コントローラ内で require ‘oauth/signature/rsa/sha1’ を行います。

また check_signature メソッド内にてコールされている validate メソッド(OpenSocial::Auth#validate(…)) では opensocial client が内部で用いている oauth ライブラリの Consumer クラスのコンストラクタに options を渡せない(署名方式を指定できない)ので、その validate メソッドを用いず、新たに validate メソッドを作成します。

上記全てを反映した gifts コントローラは下のようになります。

class GiftsController < ApplicationController
  include OpenSocial::Auth

  require 'oauth/signature/rsa/sha1'


# 公開鍵のハードコーディング
CERT = <<"EOS"
-----BEGIN CERTIFICATE-----
MIIDHDCCAoWgAwIBAgIJAMbTCksqLiWeMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV
BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIG
A1UEChMLR29vZ2xlIEluYy4xDjAMBgNVBAsTBU9ya3V0MQ4wDAYDVQQDEwVscnlh
bjAeFw0wODAxMDgxOTE1MjdaFw0wOTAxMDcxOTE1MjdaMGgxCzAJBgNVBAYTAlVT
MQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChML
R29vZ2xlIEluYy4xDjAMBgNVBAsTBU9ya3V0MQ4wDAYDVQQDEwVscnlhbjCBnzAN
BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAseBXZ4NDhm24nX3sJRiZJhvy9eDZX12G
j4HWAMmhAcnm2iBgYpAigwhVHtOs+ZIUIdzQHvHeNd0ydc1Jg8e+C+Mlzo38OvaG
D3qwvzJ0LNn7L80c0XVrvEALdD9zrO+0XSZpTK9PJrl2W59lZlJFUk3pV+jFR8NY
eB/fto7AVtECAwEAAaOBzTCByjAdBgNVHQ4EFgQUv7TZGZaI+FifzjpTVjtPHSvb
XqUwgZoGA1UdIwSBkjCBj4AUv7TZGZaI+FifzjpTVjtPHSvbXqWhbKRqMGgxCzAJ
BgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEU
MBIGA1UEChMLR29vZ2xlIEluYy4xDjAMBgNVBAsTBU9ya3V0MQ4wDAYDVQQDEwVs
cnlhboIJAMbTCksqLiWeMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA
CETnhlEnCJVDXoEtSSwUBLP/147sqiu9a4TNqchTHJObwTwDPUMaU6XIs2OTMmFu
GeIYpkHXzTa9Q6IKlc7Bt2xkSeY3siRWCxvZekMxPvv7YTcnaVlZzHrVfAzqNsTG
P3J//C0j+8JWg6G+zuo5k7pNRKDY76GxxHPYamdLfwk=
-----END CERTIFICATE-----
EOS


  # Declares the keys for your app on different containers. The index is the
  # incoming consumer key sent in the signed makeRequest from the gadget.
  # These are sample gadget credentials and should be replaced with the HMAC
  # keys granted to your own gadget.
  KEYS = {
    'XXXXXXXXXX' => {
      :secret => 'XXXXXXXXXXXXXXXXXXXXXXXXXX',
      :outgoing_key => 'orkut.com:XXXXXXXXXX',
      :container => OpenSocial::Connection::ORKUT
    }
  }

  # Declares where your application lives. Points to the page where users are
  # redirected after loading the iframe.
  SERVER = 'http://www.example.com/'

  # Controls the auto-validation of the signed makeRequest.
  before_filter :check_signature, :only => [:iframe]

  # Renders pages with boilerplate HTML/CSS/JS.
  layout 'default'

  # Implicitly checks the signature of the incoming request and saves the
  # owner id, viewer id, and consumer key in a temporary session for later use.
  # The session id is returned as an iframe snippet that the gadget can use to
  # render the app.
  def iframe
    session[:id] = params[:opensocial_owner_id]
    session[:viewer] = params[:opensocial_viewer_id]
    session[:consumer_key] = params[:oauth_consumer_key]

    render :text => "<iframe width='98%' height='600px' frameborder='0' src='#{SERVER}?sessid=#{session.model.session_id}' />"
  end

  # Loads the temporary session referenced by sessid, copies the values into
  # a persistent session that will be used to actually drive the app, and
  # deletes the temporary session (to prevent replay). Then, initiates a
  # connection to the given container and renders the appropriate content.
  def index

    proxied_session = session.model.class.find(:first,
                        :conditions => ['session_id = ?', params[:sessid]])

    if params[:sessid] && proxied_session
      session[:id] = proxied_session.data[:id]
      session[:viewer] = proxied_session.data[:viewer]
      session[:consumer_key] = proxied_session.data[:consumer_key]
      proxied_session.destroy
    end

    c = OpenSocial::Connection.new(:container => KEYS[session[:consumer_key]][:container],
                                   :consumer_key => KEYS[session[:consumer_key]][:outgoing_key],
                                   :consumer_secret => KEYS[session[:consumer_key]][:secret],
                                   :xoauth_requestor_id => session[:id])

    if session[:id] == session[:viewer]
      render_owner(c)
    elsif session[:viewer] && session[:id] != session[:viewer]
      render_viewer_with_app(c)
    else
      render_viewer_without_app(c)
    end
  end

  # Sends a gift on behalf of the viewer. If the viewer is the owner, the gift
  # is sent to the selected friend (but doesn't check to confirm that the owner
  # is friends with the specified ID). If the viewer is not the owner, the gift
  # is sent to the owner. Then the app redirects to the index.
  def give
    gift = Gift.new(params[:gift])
    if session[:id] != session[:viewer]
      gift.sent_by = session[:viewer]
      gift.received_by = session[:id]
    else
      gift.sent_by = session[:id]
    end

    if gift.save
      redirect_to :action => :index
    else
      flash[:error] = 'Error giving gift.'
      redirect_to :action => :index
    end
  end

  private

  # If the viewer is also the owner, this will render the list of gifts the
  # owner has sent or received. The owner can elect to send a gift to a friend.
  def render_owner(c)
    @id = session[:id]
    @gifts = Gift.find(:all,
                       :conditions => ['sent_by = ? OR received_by = ?', @id, @id],
                       :order => 'created_at DESC', :limit => 10)
    @gift_names = GiftName.find(:all)

    @people = fetch_gift_givers_and_friends(@id, @gifts, c)
    @owner = @people.delete(@id)

    render :action => "owner"
  end

  # If the viewer has the app installed, this will render the list of gifts
  # exchanged between the owner and viewer. The viewer can elect to send a gift
  # to the owner.
  def render_viewer_with_app(c)
    @id = session[:viewer]
    @owner_id = session[:id]

    @gifts = Gift.find(:all,
                       :conditions => ['(sent_by = ? AND received_by = ?) OR ' +
                                       '(sent_by = ? AND received_by = ?)',
                                       @owner_id, @id, @id, @owner_id],
                       :order => 'created_at DESC', :limit => 10)
    @gift_names = GiftName.find(:all)

    @viewer = OpenSocial::FetchPersonRequest.new(c, @id).send
    @owner = OpenSocial::FetchPersonRequest.new(c).send

    render :action => "viewer_with_app"
  end

  # If the viewer doesn't have the app installed, this will render the list of
  # gifts the owner has sent or received (without notes). No gifts may be sent.
  def render_viewer_without_app(c)
    @id = session[:id]
    @gifts = Gift.find(:all,
                       :conditions => ['sent_by = ? OR received_by = ?', @id, @id],
                       :order => 'created_at DESC', :limit => 10)
    @gift_names = GiftName.find(:all)

    @people = fetch_gift_givers_and_friends(@id, @gifts, c)

    render :action => "viewer_without_app"
  end

  # Requests social data for each of the people that have sent or received a
  # gift from the owner (oid). The method first generates a unique list of
  # user ids from the list of gifts, and sends a REST request for their data.
  # Users without the app installed will trigger an exception which is caught.
  # Finally, the owner's friends are fetched (which fills in the missing spaces
  # where insufficient permissions are granted to collect the user data
  # directly) and merged with the existing data.
  # This function could be sped up considerably by using a single RPC request
  # if the container supports it.
  def fetch_gift_givers_and_friends(oid, gifts, c)
    people = {}
    ids = gifts.collect {|g| [g.sent_by, g.received_by]}.flatten.uniq
    ids.each do |id|
      begin
        r = OpenSocial::FetchPersonRequest.new(c, id)
        people[id] = r.send
      rescue OpenSocial::AuthException
      end
    end
    friends = OpenSocial::FetchPeopleRequest.new(c, oid).send
    return people.merge(friends)
  end


  # Looks up the consumer secret paired with the given consumer key and
  # hands them off to the client library authentication.
  def check_signature

    is_valid_request = false

    if params[:oauth_signature_method] == 'HMAC-SHA1'
      # 署名方式が HMAC-SHA1 
      key = params[:oauth_consumer_key]
      is_valid_request = validate(key, KEYS[key][:secret], {:signature_method => 'HMAC-SHA1'})
    elsif params[:oauth_signature_method] == 'RSA-SHA1'
      # 署名方式が RSA-SHA1
      # RSA-SHA1 では 公開鍵のみ必要なので validate メソッドの 第1引数は nil. 公開鍵は第2引数に指定する )
      is_valid_request = validate(nil, CERT, {:signature_method => 'RSA-SHA1'})
    end

    unless is_valid_request
      render :text => '401 Unauthorized', :status => :unauthorized
    end

  end


  # opensocial client の validate メソッドを用いずに新たに作成.
  def validate(key = CONSUMER_KEY, secret = CONSUMER_SECRET, options={})

    consumer = OAuth::Consumer.new(key, secret, options)
    begin
      signature = OAuth::Signature.build(request) do
        [nil, consumer.secret]
      end
      pass = signature.verify
    rescue OAuth::Signature::UnknownSignatureMethod => e
      logger.error 'An unknown signature method was supplied: ' + e.to_s
    end
    return pass
  end


end

以上で、署名方式 RSA-SHA1 を用いたリクエストが受け取れるようになります。

が!

これだけではアプリケーションとしては動作しません。

何故かというと、index メソッド内にてユーザ情報を取得するために OpenSocialコンテナ(Orkut)へリクエストを送信するための Connection クラスのオブジェクトを作成しているのですが、そこで HMAC-SHA1 のデータが必要となるからです。

外部サーバ -> OpenSocialコンテナ(Orkut) へのリクエストには HMAC-SHA1 が必須となっていますので、結局前回のブログと同様に consumer key, consumer secret が必要になります。

それぞれを取得したら、下記のようにKEY定数を変更してます。

KEYS = {
    'orkut.com' => {
      :secret => '<CONSUMER SECRET>',
      :outgoing_key => 'orkut.com:<CONSUMER KEY>',
      :container => OpenSocial::Connection::ORKUT
    }

これにてアプリケーションとして動作します。

外部サーバ -> OpenSocialコンテナ(Orkut)へのリクエストが必要ないのであれば RSA-SHA1 を用いるのが良いと思いますが、外部サーバ -> OpenSocialコンテナ(Orkut)へのリクエストも必要だとすると 両方向のリクエストの署名方式に HMAC-SHA1 を用いるのが楽でいいかもしれません。

よういちろうさんが執筆された 「OpenSocial入門 ソーシャルアプリケーションの実践開発」が 2009年1月24日に発売されるとのことですので楽しみにしている今日この頃ですOpenSocialを用いたアプリ開発をされる方には必須となるのではないでしょうか。