CUI のクライアントから GAE にアクセスするバッチ的な プログラムの為に OAuth 認証を調べてみた。

GAE 側は極めて単純で

OAuthService oauthService = OAuthServiceFactory.getOAuthService();
User user = oauthService.getCurrentUser();
これだけで OAuth 認証されたユーザが取得できる。

問題はクライアント側で OAuth の 3-legged プロトコルを ちゃんと実装しなければいけない。

とっかかりはこの辺りから...

  • http://code.google.com/intl/ja/appengine/docs/java/oauth/

で、こっちにリンクされてて思いっきりのプロトコルの説明...

  • http://code.google.com/intl/ja/apis/accounts/docs/OAuth_ref.html

最初、このプロトコルを自力で実装してたのだが暗号化の辺りに来て 「こんなの普通ライブラリあるよね?」と気付いて探したら出て来た。

  • http://code.google.com/p/oauth/

こいつがまた使い方が難しくて良く分からんのだが、まぁ部品は揃ったので 進めて見る。



サーバ側の準備

まず最初に必要なのが GAE サーバの登録。
  • https://www.google.com/accounts/ManageDomains
ここにアクセスする段階で OAuth の認証されるユーザでログインが必要。

ドメイン名に .appspot.com を指定して [add domain] をクリックする。 ※普通は https にして置いた方が良いと思う。

するとサイトの所有権を確認される。

今回は meta ダグをトップページに張り付ける方法を選択した。

meta ダグを張り付けた後で [確認] をクリックすると ドメインが追加される。

ドメイン名をクリックするとドメインの管理画面に移動するので URL を入力して [Save] する。

これて OAuth の Key と Secret が登録され OAuth が使えるようになる。

テスト用のサーバ側コードを用意する。

  • https://wsjs-gae.appspot.com/test/oauth.ssjs:
var OAuthServiceFactory = Packages.com.google.appengine.api.oauth.OAuthServiceFactory; function doGet(req, res) { var oauthService = OAuthServiceFactory.getOAuthService(); var text = "user:"+oauthService.getCurrentUser()+"\n" +"admin:"+oauthService.isUserAdmin()+"\n" ; res.writer.write(text); }

これてサーバ側の準備完了。

クライアントの実装

先に見付けたサイトからライブラリの jar を落す。

  • http://code.google.com/p/oauth/downloads/list
の ApacheJMeter_oauth-v2.jar を選択。

これだけではだめなので SVN でソースは一式落したほうが良い。

svn co http://oauth.googlecode.com/svn/code/java/

サンプルコード jmeter/example/command-line を参考にクライアント側 のコードを書いてみる。

  • OAuthTestClient.java:
package org.kotemaru.test; import java.io.*; import java.net.*; import java.util.*; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import net.oauth.client.OAuthClient; import net.oauth.OAuthServiceProvider; import net.oauth.OAuthConsumer; import net.oauth.OAuthAccessor; import net.oauth.OAuth; import net.oauth.ParameterStyle; import net.oauth.OAuthMessage; import net.oauth.OAuthException; import net.oauth.http.HttpClient; import net.oauth.signature.*; import net.oauth.client.httpclient3.HttpClient3; //import net.oauth.client.URLConnectionClient; public class OAuthTestClient { public static final String APP_ID = "app_id"; public static final String CONSUMER_SECRET = "consumer_secret"; public static final String OAUTH_TOKEN = "oauth_token"; public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret"; public static final String PRIVATE_KEY = "private_key"; public static final String APP_NAME = "application_name"; public static final String GET_REQ_TOKEN = "/_ah/OAuthGetRequestToken"; public static final String GET_AUTH_TOKEN = "/_ah/OAuthAuthorizeToken"; public static final String GET_ACCESS_TOKEN = "/_ah/OAuthGetAccessToken"; public static final String HTTPS = "https://"; public static final String APPSPOT_COM = "appspot.com"; private Properties props; private String appId; private String consumerSecret; private String domain; private String reqUrl ; private String authUrl ; private String accessUrl ; private OAuthAccessor accessor; private OAuthClient client; public OAuthTestClient(Properties p) throws Exception { props = p; appId = props.getProperty(APP_ID); consumerSecret = props.getProperty(CONSUMER_SECRET); domain = appId + "." + APPSPOT_COM; reqUrl = HTTPS + domain + GET_REQ_TOKEN; authUrl = HTTPS + domain + GET_AUTH_TOKEN; accessUrl = HTTPS + domain + GET_ACCESS_TOKEN; accessor = getOAuthAccessor(); client = getOAuthClient(); } protected OAuthAccessor getOAuthAccessor() throws Exception { OAuthServiceProvider provider = new OAuthServiceProvider(reqUrl, authUrl, accessUrl); OAuthConsumer consumer = new OAuthConsumer(null, domain, consumerSecret, provider); String key = props.getProperty(PRIVATE_KEY); if (key != null) { EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec( OAuthSignatureMethod.decodeBase64(key) ); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(privKeySpec); consumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKey); consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1); } return new OAuthAccessor(consumer); } protected OAuthClient getOAuthClient() { OAuthClient client = new OAuthClient(new HttpClient3()); client.getHttpParameters().put(HttpClient.FOLLOW_REDIRECTS, Boolean.TRUE); return client; } protected OAuthMessage send(String url, String token) throws Exception { List<OAuth.Parameter> params = new ArrayList<OAuth.Parameter>(); params.add(new OAuth.Parameter(APP_NAME, appId)); params.add(new OAuth.Parameter(OAUTH_TOKEN, token)); OAuthMessage response = client.invoke(accessor, "GET", url, params); return response; } public String getAuthUrl() throws Exception { client.getRequestToken(accessor); OAuthMessage response = send(authUrl, accessor.requestToken); props.setProperty(OAUTH_TOKEN, accessor.requestToken); props.setProperty(OAUTH_TOKEN_SECRET, accessor.tokenSecret); return response.URL; } public void access() throws Exception { accessor.tokenSecret = props.getProperty(OAUTH_TOKEN_SECRET); OAuthMessage response = send(accessUrl, props.getProperty(OAUTH_TOKEN)); props.setProperty(OAUTH_TOKEN, response.getParameter(OAUTH_TOKEN)); props.setProperty(OAUTH_TOKEN_SECRET, response.getParameter(OAUTH_TOKEN_SECRET)); } public OAuthMessage request(String url) throws Exception { accessor.consumer.setProperty(OAuthClient.PARAMETER_STYLE, ParameterStyle.AUTHORIZATION_HEADER); accessor.tokenSecret = props.getProperty(OAUTH_TOKEN_SECRET); List<OAuth.Parameter> params = new ArrayList<OAuth.Parameter>(); params.add(new OAuth.Parameter(APP_NAME, appId)); params.add(new OAuth.Parameter(OAUTH_TOKEN, props.getProperty(OAUTH_TOKEN))); OAuthMessage response = client.invoke(accessor, "GET", url, params); return response; } //------------------------------------------------------------------ public static Properties loadProps(String name) throws IOException { Properties props = new Properties(); InputStream in = new FileInputStream(name); try { props.load(in); } finally { in.close(); } return props; } public static void saveProps(String name, Properties props) throws IOException { OutputStream out = new FileOutputStream(name); try { props.store(out, ""); } finally { out.close(); } } public static void main(String[] args) throws Exception { Properties props = loadProps(args[0]); System.out.println(props); OAuthTestClient test = new OAuthTestClient(props); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); System.out.print("\n> "); String line = reader.readLine(); while (line != null) { line = line.trim(); if ("request".equals(line)) { String url = test.getAuthUrl(); System.out.println("Auth URL:\n"+url); saveProps(args[0], props); } else if ("access".equals(line)) { test.access(); saveProps(args[0], props); System.out.println("Get access token."); } else { OAuthMessage response = test.request(line); System.out.println(response.readBodyAsString()); } System.out.print("\n> "); line = reader.readLine(); } } }

通常の OAuth クライアントなので GAE としての特別な部分は以下の3行だけ。

public static final String GET_REQ_TOKEN = "/_ah/OAuthGetRequestToken"; public static final String GET_AUTH_TOKEN = "/_ah/OAuthAuthorizeToken"; public static final String GET_ACCESS_TOKEN = "/_ah/OAuthGetAccessToken";

使い方は最初に目的サーバの app-id と OAuth secret を持つ プロパティファイルを作成する。

  • oauth.props:
app_id=<GAEのAPP-ID> consumer_secret=XXXXXXXXXXXXXXXXXXXX

oauth.props を引数に渡して org.kotemaru.test.OAuthTestClient を起動するとプロンプトが出るのでコマンドを入力。

$ java -classpath $CP org.kotemaru.test.OAuthTestClient oauth.props
  • トークン取得:
> request
Auth URL:
https://www.google.com/accounts/OAuthAuthorizeToken?application_name=wsjs-gae&oauth_token=4%2FtdHeN0V_wnRNUBbLJYpgiU0GRUUJ&oauth_consumer_key=wsjs-gae.appspot.com&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1302937883&oauth_nonce=1302937883290438000&oauth_version=1.0&oauth_signature=XXXXXXXXXXXXXXXXXXX%3D&scope=appengine&hd=default

このURLをブラウザで開いて登録ユーザでログインしアクセス許可を行う。

  • アクセストークンの取得:
> access Get access token.

アクセストークンは oauth.props に書き戻される。

  • 目的URLにアクセス:
> https://wsjs-gae.appspot.com/test/oauth.ssjs
user:xxxxx@gmail.com
admin:true

ちゃんと認証できました。

なんだかえらい大変でした。 趣味で使う分には固定パスワードで十分だと思います。 業務用だと管理ツールで必須とか言われるかもねー。

コード一式(Eclipseプロジェクト):
oauth-client.zip

参考にしたページ:
http://blog.smartnetwork.co.jp/staff/node/53