2010年10月24日

"oauth-signpost" で OAuth on Slim3

Slim3 を使ったアプリケーションで OAuth の認可を実施します。 OAuth (3-way dance) の流れは次のようになりますので、 HTTP リクエストの生成、認可のページへのリダイレクト、コールバックの受け取り、の三つの機能をサポートする必要があります。

OAuth の流れ

  1. サービスプロバイダーの情報を与える。
  2. アプリケーションの認証情報 (Consumer Key / Consumer Secret) を与える。
  3. HTTP リクエストによって一時トークンを取得し、 oauth_tokenoauth_token_secret を抜き出す。
  4. oauth_token を GET パラメータに乗せて認可のページへユーザーをリダイレクトさせる。
  5. サービスのサイトでユーザーに自分で認可を確認してもらう。
  6. 認可のページからコールバックへ戻してもらい、アプリケーション側で確認コード (oauth_verifier) を取得する。
  7. 確認コードを使って HTTP リクエストを発行し、認証トークンを取得する。
  8. 認証トークンから oauth_tokenoauth_token_secret を抜き出して保存する。

最後の手順で保存した認証トークンを使うと、API 経由でデータを操作できるようになります。 Twitter OAuth on Java AppEngine というブログ記事を参考にして、実際に API 経由でデータを取得します。 OAuth って何?という場合は Twitter の開発者向けページ を読みます。 英語で書いてある...とか、そういう細かいことは気にしません。 必要なら RFC などを読みます、、、これも英語なので、やっぱり日本語で検索するのが無難かもしれません。

何を言っているのか分からん...という場合は OAuth 2.0 の仕様が固まってライブラリが充実してくるのを待った方が 良いかもしれません。というか、OAuth 2.0 のドラフト 9 か 10 に対応したサービスを使ってみるのも良いですね。 いずれにしても、3-way ダンスはユーザーエージェント (Web ブラウザ) のリダイレクトを使いますので、 誰が誰から何を許可してもらうかを把握しておかないと大変です。 登場人物が3人 (サービスプロバイダー、コンシューマー or アプリケーション、ユーザー or リソースオーナー) いますので、 それぞれの関係を間違えないことが大事なのではないでしょうか。

いくつかの準備

まず、Java のライブラリをプロジェクトに配置します。 必要なライブラリは次のふたつです。違うライブラリを使う場合は適当に揃えます。 Twitter 以外のサービスプロバイダーも使いたくなるかもしれませんので、 Twitter4j は使いません。

これらの .jar ファイルを war/WEB-INF/lib ディレクトリに配置し、 CLASSPATH を通します。 適切に設定すると、Eclipse の .classpath は次のように変化します。

$ svn diff .classpath
(snip)
+       <classpathentry kind="lib" path="war/WEB-INF/lib/signpost-core-1.2.1.1.jar"/>
+       <classpathentry kind="lib" path="war/WEB-INF/lib/commons-codec-1.4.jar"/>
(snip)

次に、サービスプロバイダーの情報を取得します。 サービスプロバイダーが発行するドキュメントに記載されている、次の3つの URL を確認します。

  • 一時トークンを取得する URL ... Twitter の場合は https://api.twitter.com/oauth/request_token
  • 認証トークンを取得する URL ... Twitter の場合は https://api.twitter.com/oauth/access_token
  • 認可の画面の URL ... Twitter の場合は https://api.twitter.com/oauth/authorize

サービスプロバイダーにアプリケーションを登録すると、認証情報としてコンシューマートークンをもらえます。 表記はいろいろあるかもしれませんが、アプリケーションのためのパスワードのようなものです。 これらが揃うと、次のような Java のコードを記述できます。 記述を省略している部分には適切な文字列を当てはめてください。

String requestTokenUrl = ...;
String accessTokenUrl = ...;
String authorizeUrl = ...;
OAuthProvider provider = new DefaultProvider(requestTokenUrl, accessTokenUrl, authorizeUrl);

String consumerKey = ...;
String consumerSecret = ...;
OAuthConsumer consumer = new DefaultConsumer(consumerKey, consumerSecret);
  1. それぞれのパラメータはサービスプロバイダーの設定に合わせます。
  1. コンシューマーシークレットは秘密のパスワードなので公開してはいけません。パスワードを晒すようなものです。

次に、App Engine でセッションを有効にします。 セッションを有効にするには appengine-web.xml を次のように編集します。

$ svn di war/WEB-INF/appengine-web.xml
(snip)
-       <sessions-enabled>false</sessions-enabled>
+       <sessions-enabled>true</sessions-enabled>
(snip)

ここまでの状態でコンパイルが通れば準備は完了です。

  1. セッションを有効にしていると CNMV というデータビューアーがデフォルト設定では動きません。

実装 (前半戦)

冒頭の手順の 1 と 2 は先の準備で実施しました。 次は、HTTP リクエストを発行します。 AppEngine のドキュメントの Using java.net も参照してください。

まずは一時トークンを取得します。Java のソースコードで OAuthProvider#retrieveRequestToken を呼び出します。 このメソッド内で HTTP リクエストが発行されます。ネットワーク接続を確認しておいてください。

OAuthProvider provider = getProvider(); // 記述を簡略化しておきます。
OAuthConsumer consumer = getConsumer(); // 記述を簡略化しておきます。
try {
    String authUrl = provider.retrieveRequestToken(consumer, basePath + "/callback");
    System.out.println("tokenKey     = " + consumer.getToken());
    System.out.println("tokenSecret  = " + consumer.getTokenSecret());
    System.out.println("AuthorizeURL = " + authUrl);
} catch (OAuthNotAuthorizedException e) {
    System.out.println(e.getMessage());
}

見たままですね。戻り値が認可の画面の URL で、 consumer インスタンスには tokentokenSecret が設定されています。 ここで取得した token / tokenSecret のペアは後で使いますので、一時的な記憶領域に保存します。 本当にワンステップずつ実行したい場合は紙にメモしても構いませんが、ここではセッションに保存します。 そのためにセッションを有効した、ということです。

上記のコードスニペットを次の要領で書き換えます。 Slim3 でセッションに積むためには sessionScope() を使うのがお作法のようですが、 後で実装を書き換えますので標準のセッションオブジェクトにしておきます。手抜きです。

-  System.out.println("tokenKey     = " + consumer.getToken());
-  System.out.println("tokenSecret  = " + consumer.getTokenSecret());
+  request.getSession().setAttribute("tokenKey", consumer.getToken());
+  request.getSession().setAttribute("tokenSecret", consumer.getTokenSecret());

次に、認可のページにアクセスします。 コンソールに出力された AuthorizeURL がそれに該当しますので、これをコピーしてブラウザでアクセスします。 実際は、これだと運用に耐えませんので、ブラウザ (ユーザーエージェント) にリダイレクトするよう伝えます。 Slim3 では redirect() メソッドで返される Navigation インスタンスのことです。

return redirect(authUrl + "&oauth_callback=" + basePath + "/callback");

これで、サービスプロバイダーの認可のページに辿り着くことができました。あと二息といったところです。 冒頭の手順では、3 と 4 を消化したことになります。

手順 5 にはプログラムは関知しませんので、素直に認可してもらうとしましょう。 しかし、認可が終わったらサービスプロバイダーは何をすべきでしょうか?アプリケーションは何を期待すべきでしょうか? その答えがコールバックであり、OAuth がダンスと言われる部分です。 redirect() メソッドでシレっと oauth_callback を指定している部分でもあります。 続きを後半戦で見て行きます。

実装 (後半戦)

前半戦で、ユーザーはサービスプロバイダーのサイトにリダイレクトされました。 この時点で自分のアプリケーションではユーザーがどこのだれであるかは分かりません。 ユーザーがサービスプロバイダーから自分のシステムに戻ってきても、クッキーはドメインを超えて共有できませんので、 相変わらずユーザーがどこのだれであるかは分かりません。

しかし、先ほど保存した一時トークンと、サービスプロバイダーから戻ってくるときにユーザーが持っているコードを合わせると、この状況を打破できます。 それが OAuth のダンスです。 アプリケーションはサービスプロバイダーから認められて、ユーザーからも認められたのです。 あとは、ユーザーがきちんと自分のアプリケーションに戻ってきてくれたことをサービスプロバイダーに伝えるだけです。

実装は次のようになります。 セッションに保存しておいた一時トークンを consumer に設定し直します。 サービスプロバイダーはコールバックで戻すときにクエリパラメータに文字列を設定しますので、それを受け取ります。 コールバックが設定されていない場合にはサービスプロバイダーは PIN コードを表示するだけのはずですので、 アプリケーションにはそれを入力してもらうための機構が必要になります。 ここでの実装は前者です。 認証トークンを取得するためには Java のソースコードで OAuthProvider#retrieveAccessToken を呼び出します。

Object key = request.getSession().getAttribute("tokenKey");
Object secret = request.getSession().getAttribute("tokenSecret");
if (key == null || secret == null) {
    // invalid state
    return redirect("/");
}

OAuthConsumer consumer = getConsumer();
consumer.setTokenWithSecret(key.toString(), secret.toString());

// サービスプロバイダーからの伝言メモみたいなもの
String verifier = asString("oauth_verifier");

OAuthProvider provider = getProvider();
try {
    //provider.setOAuth10a(true);
    provider.retrieveAccessToken(consumer, verifier);
    System.out.println("tokenKey: " + consumer.getToken());
    System.out.println("tokenSecret: " + consumer.getTokenSecret());
} catch (OAuthNotAuthorizedException e) {
    System.out.println(e.getMessage());
}

認証リクエストの後では、 consumer に設定された tokentokenSecret が変更されています。 これがユーザーの認証トークンです。 最後にこれを Google App Engine の Datastore に保存します。 モデルは適当に定義します。Google アカウントと関連づけるのが簡単だと思います。

ということで OAuth による認可は完了です。

実装 (延長戦)

OAuth で認可するそもそもの目的は、サービスプロバイダーが提供するリソースにアクセスするためです。 最後に確認の意味を込めて、単発のリクエストを発行します。 エンドポイントの URL は適当に設定してください。

String tokenKey = "<access token key>";
String tokenSecret = "<access token secret>";

OAuthConsumer consumer = getConsumer();
consumer.setTokenWithSecret(tokenKey, tokenSecret);

String endpoint = "<service provider endpoint url>";
URL url = new URL(endpoint);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
    // 署名を生成します。リクエスト情報のフィンガープリントのようなものです。
    consumer.sign(connection);
    connection.connect();
    if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
        System.out.println("response is HTTP_OK!!");
    }
    InputStream stream = connection.getInputStream();
    InputStreamReader reader = new InputStreamReader(stream);
    BufferedReader buffer = new BufferedReader(reader);
    String line;
    while ((line = buffer.readLine()) != null) {
        System.out.println(line);
    }
    buffer.close();
    reader.close();
    stream.close();
} catch (OAuthNotAuthorizedException e) {
    System.out.println(e.getMessage());
}

リクエストが成功して、期待する出力を得られれば成功です。 あとは自由にリソースで遊ぶだけですね :D

  1. リクエストに失敗した場合にはエラーの oauth_problem を確認します。だいたいは signature_invalid だと思いますので、 リクエストを発行するマシンの時計が正常な状態であるかを確認します。

まとめ

Slim3 に固有の機構はあまり使っていませんが、とりあえずこれで Google App Engine を使って OAuth のダンスを踊れるようになりました。 抜粋したスニペットを例示していますので、一部おかしいコードもあるかもしれません。 それは組み込むときに修正する、ということで。

それにしても Java のコード記述量は多いなぁ、と実感する今日この頃。 IDE でスムーズに記述できるのは嬉しい人も多いでしょうが、文字数 (タイプ数ではない) が多いと、コードレビューが大変だろうなぁ、と思います。

コメントを投稿