unoh.github.com

JavaライブラリでOAuth認証

2010-08-12 10:28:17 +0000

みなさん、こんにちは。 7月にウノウに入社しました細川です。

私は、これまでいろいろなオープンソースの恩恵にあずかってきましたが、こちらから貢献をしたことは、ほとんどありませんでした。この記事がお役に立てれば幸いです。

ウノウ入社前にはJavaを主に使っていましたので、今回は、JavaのOAuthライブラリを使う方法について書いてみたいと思います。

OAuthとは

OAuthは、セキュアな認証手段として多く使われてきています。 twitterもweb APIの認証手段として採用しています。 OAuthを使った認証を行うことで、ユーザーはアカウントやパスワードを知られることなく、第三者サービスにAPIの使用を許可することができます。また、その認証は第三者サービスに関係なく取り消すことができます。

OAuth Community http://oauth.net/

OAuthコミュニティが各種プラットフォーム向けのライブラリを公開していますので、今回は、Javaのライブラリをビルドして使用します。

Code - OAuth http://oauth.net/code/

Repository http://oauth.googlecode.com/svn/code/

OAuth Library をビルドする

Eclipseを使ったビルドを紹介します。

新規プロジェクトを生成し、ショートカットメニューから「インポート」を選択します

import_project_from_svn.JPG

SVNからプロジェクトを選択」を選択し、次へ

check_out_from_svn.JPG
ロケーションにhttp://oauth.googlecode.com/svn/code/ を指定します。

select_check_out_folder.JPG
フォルダ「java」を選択します。

select_check_out_option.JPG
チェックアウトオプションは上画像のように選択してください。

チェックアウトできましたら、次にプロジェクトの設定を行います。
build_path_source.JPGのサムネール画像
Default output folderに「oauth/bin」を設定します。
また、Sourceタブから上画像のようにフォルダをパスに設定します。

build_path_libraries.JPG
LibrariesタブからSVNからチェックアウトしたlibフォルダ以下にあるライブラリをビルドパスに追加します。
これで、ライブラリがビルドされるはずです。

このままですと、classファイルがばらばらになっている状態ですので、Fat Jar Eclipse Plug-Inを使って、Jarファイルにアーカイブしましょう。インストールはこちらから。

Fat-Jarは、実行形式のJar ファイルを簡単に作れたり、参照する外部JarファイルライブラリもJar内に配置できたりする優れものですが、今回は、単純なアーカイブを作成します。

Fat-Jarをインストールしたら、プロジェクト上でショートカットメニューを開き、「Build Fat Jar」を選択します。

configure_fat_jar.JPG
Jarアーカイブの名前を設定して「Finish」で作成されます。

OAuthライブラリを使う
今回は、Twitterを例にとって、サーブレットから認証、APIアクセスを行うこととします。

Twitterのアカウントをもっている方なら、Twitterアプリケーション からアプリケーションを登録することができますので、実際に試したい場合には登録してください。
登録が完了すると、アプリケーションがOAuth認証に必要とする2つのキーと認証のためにアクセスする3つのURLが手に入ります。

これらの提供された値とOAuthライブラリの主に4つのクラスを用いて認証を行い、Twitter APIにアクセスしてみましょう。

4つのクラス
net.oauth.client.OAuthClient
通信を行うクラス
実際に使用されるHTTP通信の実装をラップします。

net.oauth.OAuthServiceProvider
プロバイダを定義しているクラス
プロバイダから提供されるURLをラップします。

net.oauth.OAuthConsumer
コンシューマ(第三者サービス)を定義しているクラス
コンシューマ・キー、コンシューマ・シークレット、コールバックURLなど、コンシューマに結びつく値を扱います。

net.oauth.OAuthAccessor
アクセスを定義しているクラス
アクセス・トークン、リクエスト・トークン、トークン・シークレットなど、個々の認証に関わる値を扱います。

認証URLを生成する
OAuth認証において認証はTwitter(プロバイダ)が行い、その結果がリダイレクトによりサイト(コンシューマ)に通知されます。
ですから、まず、認証先へのURLを生成し、それをユーザーにクリックしてもらうか、リダイレクトして認証先に移動させる必要があります。以下に、リダイレクトにより認証先に飛ばすサーブレットのdoGetメソッドを示します。
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	
	String destUrl = request.getParameter("dest");

	OAuthClient client = new OAuthClient( new URLConnectionClient());
	OAuthServiceProvider provider = new OAuthServiceProvider( REQUEST_TOKEN_URL, AUTHORIZE_URL, ACCESS_TOKEN_URL);
	OAuthConsumer consumer = new OAuthConsumer( CALL_BACK_URL,
			CONSUMER_KEY, COMSUMER_SECRET, provider);
	OAuthAccessor accessor = new OAuthAccessor( consumer);
		
	String redirectTo = null;
	try{
		try {
 			//get request token first from Twitter.com
 			HashMap params = new HashMap();
			params.put( "oauth_callback", 
					OAuth.addParameters(accessor.consumer.callbackURL,
							"dest", destUrl));
			//get request token first from Twitter.com
			client.getRequestToken(accessor, null, params.entrySet());
		} catch (OAuthException e) {
			throw new OperationFailedException( "It failed to authenticate Twitter account", e);
		} catch (URISyntaxException e) {
			throw new OperationFailedException( "It failed to authenticate Twitter account", e);
		}
			
		//build redirect path to twitter authentication page
		redirectTo = OAuth.addParameters(
				accessor.consumer.serviceProvider.userAuthorizationURL,//auth URL(twitter.com)
				"oauth_token", accessor.requestToken//
				);
	} catch (IOException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	}

	response.sendRedirect( redirectTo);
}
最初の部分ですが、ここでは、OAuthClientをもっとも単純なURLConnectionクラスを使うように初期化しています。
OAuthClient client = new OAuthClient( new URLConnectionClient());
Jakarta Commons HttpClientのv3やv4を使用することもできます。その場合にはそれぞれnet.oauth.client.HttpClient3、net.oauth.client.HttpClient4クラスを使って初期化します。しかし、Google App Engine for Java環境では、スレッドが使えない関係で、URLConnectionClientしか動作しません。 

次に、OAuthライブラリの各クラスをTwitter(プロバイダ)から提供されたパラメータを使って初期化しています。
	OAuthServiceProvider provider = new OAuthServiceProvider( REQUEST_TOKEN_URL, AUTHORIZE_URL, ACCESS_TOKEN_URL);
	OAuthConsumer consumer = new OAuthConsumer( CALL_BACK_URL,
			CONSUMER_KEY, COMSUMER_SECRET, provider);
	OAuthAccessor accessor = new OAuthAccessor( consumer);
アプリケーション登録で手に入れたパラメータを使って各クラスを初期化しています。

次のパートでは、パラメータを積んでTwitter(プロバイダ)と通信を行い、リクエストトークンを受け取ります。
 			//get request token first from Twitter.com
 			HashMap params = new HashMap();
			params.put( "oauth_callback", 
					OAuth.addParameters(accessor.consumer.callbackURL,
							"dest", destUrl));
			//get request token first from Twitter.com
			client.getRequestToken(accessor, null, params.entrySet());
リクエストトークンは認証をリクエストする際に必要になります。 
パラメータ"oauth_callback"は、リダイレクトによるコールバックが呼び出すURLを指定します。アプリケーション登録でコールバックURLは登録しているのですが、ここではそのURLにパラメータ"dest"を追加するために使っています。"oauth_callback"を指定しない場合、アプリケーション登録で登録したURLにリダイレクトされます。

 次のパートでは、getRequestTokenでTwitter(プロバイダ)から取得されたリクエストトークンを取り出し、認証先URLを生成しています。
		//build redirect path to twitter authentication page
		redirectTo = OAuth.addParameters(
				accessor.consumer.serviceProvider.userAuthorizationURL,//auth URL(twitter.com)
				"oauth_token", accessor.requestToken//
				);
うまくいけば、ユーザーは以下のようなTwitterのページに誘導されます。Twitterと連携するWebアプリケーションを使っている方なら、見たことがあるのではないでしょうか。
twitter_auth_confirmstion.JPG
ユーザーが「許可する」もしくは「拒否する」をクリックすると、"oauth_callback"で指定したURLにリダイレクトされます。
次にコールバック先での処理を見てみましょう。

コールバックでの処理
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	
	String destUrl = request.getParameter( "dest");
	
	String requestToken = request.getParameter(OAuth.OAUTH_TOKEN);
	if( requestToken == null){
		//rejected by USER
		response.sendRedirect( destUrl);
		return;
	}
	
	String verifire = request.getParameter(OAuth.OAUTH_VERIFIER);
	if( verifire == null){
		//rejected by USER
		response.sendRedirect( destUrl);
		return;
	}
	
	OAuthClient client = new OAuthClient( new URLConnectionClient());
	OAuthServiceProvider provider = new OAuthServiceProvider( REQUEST_TOKEN_URL, AUTHORIZE_URL, ACCESS_TOKEN_URL);
	OAuthConsumer consumer = new OAuthConsumer( CALL_BACK_URL,
			CONSUMER_KEY, COMSUMER_SECRET, provider);
	OAuthAccessor accessor = new OAuthAccessor( consumer);
	
	accessor.requestToken = requestToken;
	
	try {
		HashMap params = new HashMap();
		params.put( OAuth.OAUTH_VERIFIER, verifire);
		//get access token and secret from twitter.com
		client.getAccessToken(accessor, null, params.entrySet());
	} catch (OAuthException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	} catch (URISyntaxException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	}
	
	
	//Retrieve user's information
	OAuthMessage oMessage = null;
	try {
		oMessage = accessor.newRequestMessage("GET", "http://api.twitter.com/1/account/verify_credentials.json", null);
	} catch (OAuthException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	} catch (URISyntaxException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	}

	OAuthResponseMessage rMessage = client.access(oMessage, ParameterStyle.AUTHORIZATION_HEADER);
	int status = rMessage.getHttpResponse().getStatusCode();
	if( status == HttpResponseMessage.STATUS_OK){
		String jsonStr = rMessage.readBodyAsString();
		JSONObject jObj = JSONObject.fromObject(jsonStr);
		String userId = jObj.optString("id");
		accessor.setProperty( "id", userId);
	}else{
		throw new RuntimeException( "It failed to authenticate Twitter account STATUS CODE:"+status);
	}
	
	//Store access token and secret to Cookie
	TwitterAPIUtil.storeTokenToCookie(accessor, response);
	
	response.sendRedirect( destUrl);
}  	
まず、認証によりTwitter(プロバイダ)から付け加えられたパラメータを取得しています。
	String requestToken = request.getParameter(OAuth.OAUTH_TOKEN);
	if( requestToken == null){
		//rejected by USER
		response.sendRedirect( destUrl);
		return;
	}
	
	String verifire = request.getParameter(OAuth.OAUTH_VERIFIER);
	if( verifire == null){
		//rejected by USER
		response.sendRedirect( destUrl);
		return;
	}
これらがセットされていなかった場合、ユーザーが認証を拒否したと考えられますので、あらかじめコールバックURLに追加したパラメータに積んでおいた飛び先URLにリダイレクトさせます。

先の処理と同様にOAuthのクラスを初期化後、認証により手に入ったリクエストトークン、ベリファイアを使ってTwitter(プロバイダ)からアクセストークンとトークンシークレットを取得します。
	accessor.requestToken = requestToken;
	
	try {
		HashMap params = new HashMap();
		params.put( OAuth.OAUTH_VERIFIER, verifire);
		//get access token and secret from twitter.com
		client.getAccessToken(accessor, null, params.entrySet());
	} catch (OAuthException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	} catch (URISyntaxException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	}
アクセストークンとトークンシークレットはAPIをコールするために必要になります。 うまく取得できた場合、すでにOAuthAccessorオブジェクトにアクセストークンとトークンシークレットは格納されていますので、Twitter APIを呼び出すことができます。
	//Retrieve user's information
	OAuthMessage oMessage = null;
	try {
		oMessage = accessor.newRequestMessage("GET", "http://api.twitter.com/1/account/verify_credentials.json", null);
	} catch (OAuthException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	} catch (URISyntaxException e) {
		throw new RuntimeException( "It failed to authenticate Twitter account", e);
	}

	OAuthResponseMessage rMessage = client.access(oMessage, ParameterStyle.AUTHORIZATION_HEADER);
	int status = rMessage.getHttpResponse().getStatusCode();
	if( status == HttpResponseMessage.STATUS_OK){
		String jsonStr = rMessage.readBodyAsString();
		JSONObject jObj = JSONObject.fromObject(jsonStr);
		String userId = jObj.optString("id");
		accessor.setProperty( "id", userId);
	}else{
		throw new RuntimeException( "It failed to authenticate Twitter account STATUS CODE:"+status);
	}
ここでは、認証済みユーザー自身のユーザー情報を返す account/verify_credentials APIを呼び出しています。レスポンスのJSONを扱うのには、Json-lib ライブラリを使用しています。

最後に認証情報をクッキーに保存しています。
	//Store access token and secret to Cookie
	TwitterAPIUtil.storeTokenToCookie(accessor, response);
このメソッドの中身は以下のようになっています。
public static void storeTokenToCookie( OAuthAccessor accessor, HttpServletResponse response, int maxAge){
	Cookie cookie = new Cookie( accessor.consumer.consumerKey+"_requesttoken", accessor.requestToken);
	cookie.setPath( "/" );
	cookie.setMaxAge( maxAge);
	response.addCookie( cookie);
	
	cookie = new Cookie( accessor.consumer.consumerKey+"_accesstoken", accessor.accessToken);
	cookie.setPath( "/" );
	cookie.setMaxAge( maxAge);
	response.addCookie( cookie);

	cookie = new Cookie( accessor.consumer.consumerKey+"_secret", accessor.tokenSecret);
	cookie.setPath( "/" );
	cookie.setMaxAge( maxAge);
	response.addCookie( cookie);
	
	String userId = (String)accessor.getProperty("id");
	if( userId == null){
		userId = "";
	}
	cookie = new Cookie( accessor.consumer.consumerKey+"_id", userId);
	cookie.setPath( "/" );
	cookie.setMaxAge( maxAge);
	response.addCookie( cookie);
}
認証情報は、もちろんデータベースなどのストレージに保存することも可能です。しかし、これらの認証情報は、Twitter(プロバイダ)のサイトでユーザーが一方的に無効にできることには留意しておく必要があるでしょう。

別の場所で認証情報を使ってAPIを呼び出すには、以下のようにします。
	.........
	OAuthClient client = new OAuthClient( new URLConnectionClient());
	OAuthServiceProvider provider = new OAuthServiceProvider( REQUEST_TOKEN_URL, AUTHORIZE_URL, ACCESS_TOKEN_URL);
	OAuthConsumer consumer = new OAuthConsumer( CALL_BACK_URL,
			CONSUMER_KEY, COMSUMER_SECRET, provider);
	OAuthAccessor accessor = new OAuthAccessor( consumer);
	
	//try to retrieve token
	Cookie[] cookies = request.getCookies();
	for( int i = 0; i < cookies.length; ++i){
		Cookie cookie = cookies[i];
		if( cookie.getName().equals( accessor.consumer.consumerKey+"_accesstoken")){
			accessor.accessToken = cookie.getValue();
		}else if( cookie.getName().equals( accessor.consumer.consumerKey+"_requesttoken")){
			accessor.requestToken = cookie.getValue();
		}else if( cookie.getName().equals( accessor.consumer.consumerKey+"_secret")){
			accessor.tokenSecret = cookie.getValue();
		}else if( cookie.getName().equals( accessor.consumer.consumerKey+"_id")){
			accessor.setProperty( "id", cookie.getValue());
		}
	}
	.........
非常に駆け足になってしまいましたが、どのような印象を受けましたか?ライブラリを使用することで、かなりOAuthへの敷居が下がったと感じた方もおられるのではないでしょうか?

この記事がちょっとでもだれかのお役に立てば幸いです。