こんばんは。これは、D言語 Advent Calendar 2012の20日目の記事です。D言語でOAuthなAPI呼び出しをしたい、という人が世の中には居るそうです。という話をします。
##OAuth認証
OAuth認証とは、平和と慈愛に満ちたコンピュータ様の与えて下さる庇護と恩恵であり、それを幸福と感じることは善良な市民たるあなたの義務です。
今どきのWeb APIなんかがこぞって採用していますが、残念なことにD言語にはOAuthライブラリが無いらしいです。仕方ないね。
####トークン
とりあえず、keyとsecretのペアをトークンと呼ぶことにします。などと意味不明な供述をしており、正確な意味はわかりません。
name | description |
---|---|
consumer token (consumer key, consumer secret) | アプリケーション固有のトークン |
request token (request key, request secret) | access token を取得するために必要、使い捨て |
access token (access key, access secret) | ユーザ固有のトークン、ユーザIDとパスワードの代わり |
最終的に必要なのは、consumer token と access token の2つです。
####手順
ややこしいです。死にます。
- アプリケーション登録をして consumer token をもらう
- consumer token を使って request token をもらう
- 認証用URIに request key を付加したURIを開き、PIN番号をもらう
- consumer token と request token とPIN番号を使い、access token をもらう
- consumer token と access token を使ってAPIとキャッキャウフフする
####パラメータ
ややこしいです。死にます。
key | value |
---|---|
oauth_consumer_key | アプリケーションの登録をしたときに貰えるconsumer key |
oautn_nonce | 適当な文字列、例では面倒なのでUnix時間にしてる |
oauth_signature | 署名、詳しくは後述 |
oauth_signature_method | 使用するハッシュ関数の名前、HMAC-SHA1 |
oauth_timestamp | 現在のUnix時間、タイムアウトの判定とかに使われる |
oauth_token *1 | request token とか access token とか |
oauth_verifier *2 | 名状しがたき値、PIN番号 |
oauth_version | バージョン、現在 1.0 と 2.0 がある |
これに加えて、APIごとに要求されるパラメータというものがあります。例えば、ツイーヨするときは、status
というキーに呟きたい文字列を渡します。後は、各サービスのリファレンスでも食べてください。
*1 手順4で request token 、それ以降は access token が入る
*2 手順4でしか使わない
##認証付き呼び出し
説明するのが億劫なのでコードを載せます。察してください。
今回は、通信にcurlを使用します。また、以下の通りのインポートと構造体の定義をしているものとします。
import std.algorithm, std.array, std.base64, std.conv, std.datetime, std.digest.sha, std.string, std.typecons, std.uri;
import std.net.curl, std.stdio;
alias Tuple!(string, "key", string, "secret") Token;
####署名の生成
トークンとHTTPメソッド名とURIとパラメータから生成します。
// パラメータをキーでソートし、キーと値をそれぞれ=と&で繋ぐ
auto query = param.keys.sort.map!(k => k ~ "=" ~ param[k]).join("&");
// consumer secretとtoken secretをそれぞれURLエンコードし、&で繋ぐ
auto key = [consumer.secret, token.secret].map!encodeComponent.join("&");
// メソッド名とURIと上記のqueryをそれぞれURLエンコードし、&で繋ぐ
auto base = [method, uri, query].map!encodeComponent.join("&");
// HMAC-SHA1ハッシュ関数にkeyとbaseを渡してダイジェスト値を求め、Base64エンコードし、URLエンコードする
auto signature = encodeComponent(cast(immutable)Base64.encode(hmac_sha1(key, base)));
hmac_sha1
関数は、std.digest.sha.sha1Of
関数を利用して自分で作成したものです。実装は後で出します。
####OAuthな呼び出し
署名などをHTTPヘッダのAuthorizationフィールドに乗せて、HTTPメソッドを繰り出します。
// 適当にAPIの呼び出しに使うパラメータ
auto param = ["mami": "candeloro", "saya": "oktavia", "mado": "kriemhild gretchen"];
// OAuthなパラメータをつっこむ
auto parameter = ["oauth_consumer_key": consumer.key,
"oauth_nonce": Clock.currTime.toUnixTime.to!string,
"oauth_signature_method": "HMAC-SHA1",
"oauth_timestamp": Clock.currTime.toUnixTime.to!string,
"oauth_version": "1.0"];
// パラメータ同士を合わせる
// ついでに日本語来ると困るのでURLエンコードしとく
foreach (k, v; param)
parameter[k] = v.encodeComponent;
// token keyが存在しているならパラメータに含める
if (!(token.key is null))
parameter["oauth_token"] = token.key;
// 署名を生成し、パラメータに含める
auto signature = ……;
parameter["oauth_signature"] = signature;
// OAuthなパラメータを抜き出し、それぞれ=と,で繋ぐ
auto authorize_keys = parameter.keys.filter!q{a.countUntil("oauth_")==0};
auto authorize = "OAuth " ~ authorize_keys.map!(x => x ~ "=" ~ parameter[x]).join(",");
// それ以外のパラメータは、それぞれ=と&で繋ぐ
auto option_keys = parameter.keys.filter!q{a.countUntil("oauth_")!=0};
auto option = option_keys.map!(x => x ~ "=" ~ parameter[x]).join("&");
auto http = HTTP();
// Authorizationフィールドにパラメータを渡す
http.addRequestHeader("Authorization", authorize);
// URIの後ろに残りのパラメータを付与してGET
auto result = cast(immutable)get((0 < option.length)? uri ~ "?" ~ option: uri, http);
##実装例
ツイーヨするコード。
// dmd 2.062import std.algorithm, std.array, std.base64, std.conv, std.datetime, std.digest.sha, std.string, std.typecons, std.uri;import std.net.curl, std.stdio;alias Tuple!(string, "key", string, "secret") Token;private string signature(in Token consumer, in Token token, in string method, in string uri,
in string[string] param) { auto query = param.keys.sort.map!(k => k ~ "=" ~ param[k]).join("&"); auto key = [consumer.secret, token.secret].map!encodeComponent.join("&"); auto base = [method, uri, query].map!encodeComponent.join("&"); return encodeComponent(cast(immutable)Base64.encode(hmac_sha1(key, base)));}private string signedCall(in Token consumer, in Token token, in string method, in string uri,
in string[string] param, string delegate(HTTP, in string, in string) call) { auto parameter = ["oauth_consumer_key": consumer.key, "oauth_nonce": Clock.currTime.toUnixTime.to!string, "oauth_signature_method": "HMAC-SHA1", "oauth_timestamp": Clock.currTime.toUnixTime.to!string, "oauth_version": "1.0"]; foreach (k, v; param) parameter[k] = v.encodeComponent; if (!(token.key is null)) parameter["oauth_token"] = token.key; auto signature = signature(consumer, token, method, uri, parameter); parameter["oauth_signature"] = signature; auto authorize_keys = parameter.keys.filter!q{a.countUntil("oauth_")==0}; auto authorize = "OAuth " ~ authorize_keys.map!(x => x ~ "=" ~ parameter[x]).join(","); auto option_keys = parameter.keys.filter!q{a.countUntil("oauth_")!=0}; auto option = option_keys.map!(x => x ~ "=" ~ parameter[x]).join("&"); auto http = HTTP(); http.addRequestHeader("Authorization", authorize); return call(http, uri, option);}string signedGet(in Token consumer, in Token token, in string uri, in string[string] param = null) { return signedCall(consumer, token, "GET", uri, param, (http, uri, option) { return cast(immutable)get((0 < option.length)? uri ~ "?" ~ option: uri, http); });}string signedPost(in Token consumer, in Token token, in string uri, in string[string] param = null) { return signedCall(consumer, token, "POST", uri, param, (http, uri, option) { return cast(immutable)post(uri, option, http); });}private ubyte[] hmac_sha1(in string key, in string message) { auto padding(in ubyte[] k) { auto h = (64 < k.length)? sha1Of(k): k; return h ~ new ubyte[64 - h.length]; } const k = padding(cast(ubyte[])key); return sha1Of((k.map!q{cast(ubyte)(a^0x5c)}.array) ~ sha1Of((k.map!q{cast(ubyte)(a^0x36)}.array) ~ cast(ubyte[])message)).dup;}private Token toToken(string s) { string[string] result; foreach (x; s.split("&").map!q{a.split("=")}) result[x[0]] = x[1]; return Token(result["oauth_token"], result["oauth_token_secret"]);}void main() { immutable consumer = Token("replace your consumer key", "replace your consumer secret"); auto request_token = signedGet(consumer, Token(), "https://api.twitter.com/oauth/request_token").toToken; auto authorize_uri = "https://api.twitter.com/oauth/authorize" ~ "?oauth_token=" ~ request_token.key; authorize_uri.writeln; auto verifier = readln.chomp; auto access_token = signedGet(consumer, request_token, "https://api.twitter.com/oauth/access_token", ["oauth_verifier": verifier]).toToken; signedPost(consumer, access_token,
"https://api.twitter.com/1.1/statuses/update.json", ["status": "市民、認証は義務です"]);
}```
##終わりに
朦朧とした意識の中書いた文章とコードなのでところどころ頭が溶けている感じもしますが、
どうしてこんなことになったのか、私にはわかりません。
これをあなたが読んだなら、その時、私は寝ているでしょう。
記事があるか、ないかの違いはあるでしょうが。
これを読んだあなた。どうかライブラリを書いてください。それだけが、私の望みです。
次の21日目は、[@Fuhduki](http://qiita.com/users/Fuhduki)さんです。蛙。