ナカザンドットネット

それって私の感想ですよね

DevQuizで頑張り切れなかった記録/2-legged OAuth

9月末のGoogle Developer Day 2010への参加権を賭けたDevQuizが、昨日を以て回答期間終了となりました。
仕事の都合で僕はどっちにしろ参加できないのですが、まあ良い勉強になるだろうということで、2-legged OAuthの問題だけ解いてみました。
というかコレ以降のしりとりとかパックマンには手も足も出なかったので、正解できたOAuthくらいは自慢させてくれよ!って感じの残念な感じのエントリです。

設問

  1. POSTメソッドで
  2. http://gdd-2010-quiz-japan.appspot.com/oauth/hogehoge*1
  3. hello=world なパラメータを送信してください。
  4. OAuthのパラメータは全てAuthorizationヘッダーに含めてください。
    • realmパラメータは"devquiz"を入れること。
  5. 署名方式はHMAC-SHA1です。
  6. consumer keyはhogehoge(もちろん仮名)
  7. consumer secretはhugahuga(同上)

成功したらレスポンス200が返ります。


言語は何でも良かったらしいんですが、ぶっちゃけ僕Java以外はド初心者(Javaも初心者脱出できてませんが)なので、Javaで書きました。

コード

package oauth;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;

import com.sun.org.apache.xml.internal.security.utils.Base64;

public class DevQuiz {

	private static final String DEVQUIZ = 
			"http://gdd-2010-quiz-japan.appspot.com/oauth/hogehoge";
	private static final String CONSUMER_KEY = "hogehoge";
	private static final String CONSUMER_SECRET = "hugahuga";
	private static final String SIGNATURE_METHOD = "HMAC-SHA1";
	private static final String OAUTH_VERSION = "1.0";

	public static void main(String[] args) {
		try {
			// signature生成用のOAuthパラメータ文字列
			String requestParameters = 
						getRequestParameters();
			
			// signatureの元となる文字列を生成
			String signatureBaseString = 
						getSignatureBaseString(requestParameters);
			
			// signature生成用にconsumer secretに&を付加する
			String keyString = getKeyString();

			// signature生成
			String signature = 
						getSignature(signatureBaseString, keyString);
			
			// HTTP接続をオープン
			HttpClient httpClient = new DefaultHttpClient();
			HttpPost request = new HttpPost(DEVQUIZ);
			
			// Authorizationヘッダを登録
			String reqStr = 
					"OAuth realm=devquiz," + 
						requestParameters.replace("&", ",")+
						", oauth_signature="+signature;
			request.setHeader("Authorization",reqStr);
			
			// パラメータを登録
			final List <NameValuePair> params = 
						new ArrayList <NameValuePair>();
			params.add(new BasicNameValuePair("hello", "world"));
			request.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
			
			// リクエストを実行
			HttpResponse response = httpClient.execute(request);
			
			//レスポンスを表示
			System.out.println(response.getStatusLine());
			
			/*
			 * HTTP/1.1 200 OK
			 */
			
			request.abort();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * consumer secret文字列に&を付けて返します。
	 * 通常のOAuthでは&の後ろにtoken secretを付けますが、
	 * 2-leggedではそういうのは無いので。
	 * @return 
	 * 		consumer secret に "&"を繋げたもの
	 */
	private static String getKeyString() {
		return CONSUMER_SECRET + "&";
	}
	
	/**
	 * signatureを生成する元となる文字列を作成します。
	 * @param requestParameters 
	 * 		OAuthパラメータを&で繋いだもの
	 * @return 
	 * 		POST&【URLエンコードしたURL】&【URLエンコードしたパラメータ】
	 * 			& URLエンコードしたrequestParameters】
	 */
	private static String getSignatureBaseString(String requestParameters) {
		return "POST&" + URLEncode(DEVQUIZ) + "&hello%3Dworld%26"
				+ URLEncode(requestParameters);
	}

	/**
	 * signature生成用のOAuthパラメータ文字列を生成
	 * @return 
	 * 		OAuthパラメータを&で繋げた文字列
	 */
	private static String getRequestParameters() {
		return "oauth_consumer_key=" + CONSUMER_KEY + "&"
				+ "oauth_nonce=" + URLEncode(getNonce()) + "&"
				+ "oauth_signature_method=" + SIGNATURE_METHOD + "&"
				+ "oauth_timestamp=" + getTimeStamp() + "&" 
				+ "oauth_version=" + OAUTH_VERSION;
	}

	/**
	 * リクエストの重複回避用の任意の文字列
	 * @return 
	 * 		適当に11111111(ホントに何でもいい)
	 */
	private static String getNonce() {
		return "11111111";
	}

	/**
	 * システムクロックから時刻を取得
	 * @return 
	 * 		Long型の現在時刻
	 */
	private static String getTimeStamp() {
		return Long.toString(System.currentTimeMillis());
	}

	/**
	 * signatureBaseStringに対してkeyStringをkeyにして
	 * HMAC-SHA1方式で署名を行い、
	 * さらにBase64エンコードとURLエンコードをかけて
	 * signatureを生成する。
	 * @param signatureBaseString
	 * 		元となる文字列
	 * @param keyString
	 * 		署名のkeyとなる文字列
	 * @return 
	 * 		生成されたsignature
	 */
	private static String getSignature(String signatureBaseString,
			String keyString) {
		String signature = null;
		String algorithm = "HmacSHA1";
		try {
			Mac mac = Mac.getInstance(algorithm);
			Key key = new SecretKeySpec(keyString.getBytes(), algorithm);

			mac.init(key);
			byte[] digest = mac.doFinal(signatureBaseString.getBytes());
			signature = URLEncode(Base64.encode(digest));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		}
		return signature;
	}
	
	/**
	 * URLエンコード
	 * @param beforeEncode
	 * 		元となる文字列
	 * @return
	 * 		URLエンコードされた文字列
	 */
    public static String URLEncode(String beforeEncode) {
        String afterEncode;
        try {
            beforeEncode = beforeEncode.replace(" ", "&nbsp;");
            afterEncode = URLEncoder.encode(beforeEncode, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
        return afterEncode;
    }
}

たぶん読めば分かるので解説とかしません。
使ったライブラリ一覧↓

  • httpclient-4.0.1.jar
  • httpcore-4.0.1.jar
  • xmlsec-2.0.jar
  • commons-logging-1.1.1.jar

*1:hogehogeは各自のconsumer keyでした