Edited at
D言語Day 20

市民、認証は義務です

More than 5 years have passed since last update.

こんばんは。これは、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 tokenaccess token の2つです。


手順

ややこしいです。死にます。


  1. アプリケーション登録をして consumer token をもらう


  2. consumer token を使って request token をもらう

  3. 認証用URIに request key を付加したURIを開き、PIN番号をもらう


  4. consumer tokenrequest token とPIN番号を使い、access token をもらう


  5. consumer tokenaccess 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.02.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)さんです。蛙。