OpenSocial OAuth の実装を追いかける。

書いている最中に下書き保存して休憩したら、下書きが消えたので絵だけ復元しつつ。。。やる気レス。

前回は http://d.hatena.ne.jp/yssk22/20090722#1248283792 の辺りで、partuza で大体 /people/{guid}/{selector}/{pid} の例 がどう取り扱われるかわかったので、@me とかその辺をどう解釈するのか、っていうことを追求しています。

すなわち、partuzaとshindigソースコードからOpenSocialの認証・認可周りで何やってんの?っていう話です。


src/social/servlet/ApiServlet.php から。

// ****
// src/social/servlet/ApiServlet.php
// line: 102
// ****
  public function getSecurityToken() {
    // see if we have an OAuth request
    $request = OAuthRequest::from_request();
    $appUrl = $request->get_parameter('oauth_consumer_key');
    $userId = $request->get_parameter('xoauth_requestor_id'); // from Consumer Request extension (2-legged OAuth)
    $signature = $request->get_parameter('oauth_signature');
    if ($appUrl && $signature) {
      //if ($appUrl && $signature && $userId) {
      // look up the user and perms for this oauth request
      $oauthLookupService = Config::get('oauth_lookup_service');
      $oauthLookupService = new $oauthLookupService();
      $token = $oauthLookupService->getSecurityToken($request, $appUrl, $userId, $this->getContentType());
      if ($token) {
        $token->setAuthenticationMode(AuthenticationMode::$OAUTH_CONSUMER_REQUEST);
        return $token;
      } else {
        return null; // invalid oauth request, or 3rd party doesn't have access to this user
      }
    } // else, not a valid oauth request, so don't bother

とりあえずHTTP リクエスト受け取ったら、oauth_consumer_key, oauth_signature, xoauth_requestor_id を取り出します。

  • oauth_consumer_key はアプリケーションのキーです。ガジェットが使われているコンテナーでいいのかな?
  • oauth_signature はリクエストの署名、だそうで。HTTP Method、URL、クエリ の組をSHA1などでHashにしたもの。http://yamashita.dyndns.org/blog/django-oauth-consumer-request/ あたりに記述があり、要するにリクエストが意図したとおりのものかを確認するために使うものに見える。
  • xoauth_signature はConsumerをしようするエンドユーザーのID、つまりガジェットを閲覧しているブラウザを使っているユーザーのIDです。

で、アプリケーションのIDと署名がある場合は、OAuthLookupService というやつで、実際のセキュリティトークンを取得する模様。Partuzaのwikiを見ると、

  'person_service' => 'PartuzaService',
  'activity_service' => 'PartuzaService',
  'app_data_service' => 'PartuzaService',
  'messages_service' => 'PartuzaService',
  'oauth_lookup_service' => 'PartuzaOAuthLookupService',
  'extension_class_paths' => '/path/to/partuza/Shindig'

という設定になるので、PartuzaOAuthLookupService を調べる。

// ****
// Shindig/PartuzaOAuthLookupService.php
// line: 36
// ****
  public function getSecurityToken($oauthRequest, $appUrl, $userId, $contentType) {
    try {
      // Incomming requests with a POST body can either have an oauth_body_hash, or include the post body in the main oauth_signature; Also for either of these to be valid
      // we need to make sure it has a proper the content-type; So the below checks if it's a post, if so if the content-type is supported, and if so deals with the 2
      // post body signature styles
      $includeRawPost = false;
      $acceptedContentTypes = array('application/atom+xml', 'application/xml', 'application/json');
      if (isset($GLOBALS['HTTP_RAW_POST_DATA']) && ! empty($GLOBALS['HTTP_RAW_POST_DATA'])) {
        if (! in_array($contentType, $acceptedContentTypes)) {
          // This is rather double (since the ApiServlet does the same check), but for us to do a meaninful processing of a post body, this has to be correct
          throw new Exception("Invalid Content-Type specified for this request, only 'application/atom+xml', 'application/xml' and 'application/json' are accepted");
        } else {

まずは、Content-Type のチェックのようで、application/atom+xml, application/xml, application/json を指定したリクエストである必要がある、と。
続いて、

          if (isset($_GET['oauth_body_hash'])) {
            // this request uses the oauth_body_hash spec extension. Check the body hash and if it fails return 'null' (oauth signature failure)
            // otherwise continue on to the regular oauth signature verification, without including the post body in the main oauth_signature calculation
            if (! $this->verifyBodyHash($GLOBALS['HTTP_RAW_POST_DATA'], $_GET['oauth_body_hash'])) {
              return null;
            }
          } else {
            // use the (somewhat oauth spec invalid) raw post body in the main oauth hash calculation
            $includeRawPost = $GLOBALS['HTTP_RAW_POST_DATA'];
          }
        }
      }

なにやらoauth_body_hash とか、その辺のバリデーション。これはあとで。続いていよいよ、データストアからOAuthを処理するところ。

      $dataStore = new PartuzaOAuthDataStore();
      if ($includeRawPost) {
        // if $includeRawPost has been set above, we need to include the post body in the main oauth_signature
        $oauthRequest->set_parameter($includeRawPost, '');
      }
      if (! isset($oauthRequest->parameters['oauth_token'])) {
        // No oauth_token means this is a 2 legged OAuth request
        $ret = $this->verify2LeggedOAuth($oauthRequest, $userId, $appUrl, $dataStore);
      } else {
        // Otherwise it's a clasic 3 legged oauth request
        $ret = $this->verify3LeggedOAuth($oauthRequest, $userId, $appUrl, $dataStore);
      }
      if ($includeRawPost) {
        unset($oauthRequest->parameters[$includeRawPost]);
      }
      return $ret;
    } catch (OAuthException $e) {
      return null;
    }
  }

結局、oauth_token というリクエストパラメーターがなければ、2-legged OAuthの処理をするし、セットされていれば、3-legged OAuthで処理するようです。2-leggedとか3-leggedとかなんやねん?という話があるんだけれど、http://sites.google.com/site/oauthgoog/2leggedoauth/2opensocialrestapi あたりや、http://d.hatena.ne.jp/lyokato/20080819/1219116960 を参考にする。完全にわかってはないんだけれど、ひとまず通常の「ユーザーによる確認プロセス」を省略する2-leggedのほうを実装すればよいらしい(確認プロセスは「ユーザーがガジェットの追加する」という行為によって代替される)。

3-legged もあるのは、Gadget 以外からの普通のリクエストも想定している、ということでしょう。で、続き。

// ****
// Shindig/PartuzaOAuthLookupService.php
// line: 94
// ****
  private function verify2LeggedOAuth($oauthRequest, $userId, $appUrl, $dataStore) {
    $consumerToken = $dataStore->lookup_consumer($oauthRequest->parameters['oauth_consumer_key']);
    $signature_method = new OAuthSignatureMethod_HMAC_SHA1();
    $signature_valid = $signature_method->check_signature($oauthRequest, $consumerToken, null, $_GET["oauth_signature"]);
    if (! $signature_valid) {
      // signature did not check out, abort
      return null;
    }
    return new OAuthSecurityToken($userId, $appUrl, $dataStore->get_app_id($consumerToken), "partuza");
  }

コンシューマーとsignatureの組を検証して、OKであれば、OAuthのセキュリティトークンを返す。OAuthSecurityToken はShindigのほうで定義されている構造体クラス。$dataStore->get_app_id($consumerToken) の実態は、PartuzaOAuthDataStore に記述されていて、

  public function get_app_id($token) {
    $token_key = mysqli_real_escape_string($this->db, $token->key);
    $res = mysqli_query($this->db, "select app_id from oauth_consumer where consumer_key = '$token_key'");
    $ret = 0;
    if (mysqli_num_rows($res)) {
      list($ret) = mysqli_fetch_row($res);
    }
    return $ret;
  }

ConsumerKeyとアプリケーションIDのマッピング表のようなテーブルがあって、そこから引っ張り出しています。

ここまで復習すると

oauth_consumer_key 内部のテーブルでappIdに変換される, appUrl としても使われる
xoauth_requestor_id userId として使われる

という具合です。上記の情報が OAuthSecurityToken として含まれています。

さて、これらがどう使われるのか、というところですが続きは明日。