Flickr の認証API
認証API をどうするか、ということで数名のスタッフであれこれ話ながらやってます。
まず、はてなの認証APIを使って何ができるといいのかというところですが、はてなラボをオープンしたときにいただいた意見などを見ると、「はてなのAPIで認証付きのをセキュアに利用するための API」というより「サードパーティのアプリケーションではてなIDでユーザーを識別できるためのAPI」の方が求められているという風に思いました。
具体的には、新規にユーザーを識別する必要のあるアプリケーション、例えば掲示板などを作るとして、その掲示板のユーザーを一意に識別する方法としてはてなIDを使いたい、そのIDが本当にその人のものであるかどうかをはてなが保証する、その保証を問い合わせるための API ですね。その掲示板でログインして何かを書き込むと id:naoya、と表示されると。
この手の認証APIを提供しているサービスは既にいくつかあるので、それらをスタッフで手分けして調べる、ということをしました。僕の担当は Flickr の認証API。
API の利用例としては、typester さんの CLON でコメントのユーザーを識別するのに Flickr API を使っていて参考になりました。
- コメントを書く前に CLON に貼られたリンクで Flickr に飛ぶ
- 「CLON で認証を要求しているけど受け入れる?」みたいな画面が Flickr 上に表示される。(Flickr でログインしていれば、聞かれるのは Yes or No だけ。パスワード入力などは要求されない)
- Yes ボタンを押すと CLON 側にリダイレクトで戻る。
- この時点で CLON が自分の Flickr アカウントを参照できる状態になる。
- コメントを書き込むと、書き手の情報として Flickr のアカウント名などが使われる。
という流れです。
この Flickr API の仕組みの方ですが、以下の二つのドキュメントがとても参考になりました。
が、どうも僕はこの手の仕様を頭の中だけで理解するのが苦手なので、実際に手を動かしながら理解してみよう、ということで Perl でコードを書きながらやってみました。以下、コードを組み立てていく手順と共に Flickr の認証APIの扱い方を記述してみます。
Flickr API を Perl で利用するには Flickr::API というモジュールを使えば簡単です。が、これをそのまま使ったのではちと理解が進まないので、Flickr::API と似たようなモジュールを自分で実装してみるということをしてみました。
まず始めにやるべきは、アプリケーションを作るときに、そのアプリケーションから Flickr の認証画面へ飛ばすためのリンクを作るという作業。http://flickr.com/services/auth/ という URL に
の三つのパラメータをクエリパラメータで指定したリンクを用意します。このために Flickr 側の管理画面で API キーを取得します。認証API を使うのであれば、その認証 API を利用するアプリケーションの名前、それから後述する認証後にリダイレクトで戻る URL (認証ハンドラのURL)などを指定する必要があります。それらの作業を済ませると、Shared Secret というまた別のキーが発行されるのでこれも覚えておきます。
さて、リンクの用意ですが、自作アプリケーションのビューを Template Toolkit で扱う場合に
<a href="[% flickr.link_to_login %]">...</a>
とするとテンプレートが展開されて該当のリンクが表示されるという風にしてみます。[% flickr.link_to_login %]
の flickr
は API を抽象化した以下のクラスのインスタンスです。
package Hatena2::Flickr::API; use strict; use warnings; use base qw(Class::Accessor); use URI; use Digest::MD5 qw(md5_hex); our $VERSION = 0.01; __PACKAGE__->mk_accessors(qw(api_key secret perms)); sub link_to_login { my $self = shift; my $uri = URI->new('http://flickr.com/services/auth/'); my $request = { api_key => $self->api_key, perms => $self->perms || '', }; $uri->query_form( %$request, api_sig => $self->api_sig($request), ); return $uri->as_string; } sub api_sig { my $self = shift; my $args = shift; my $sig = $self->secret; for my $key (sort {$a cmp $b} keys %{$args}) { my $value = $args->{$key} ? $args->{$key} : ''; $sig .= $key . $value; } return Digest::MD5::md5_hex($sig); } 1;
このクラスをインスタンス化してテンプレートに渡し、テンプレートで link_to_login
を呼び出せば OK。インスタンス化するには
Hatena2::Flickr::API->new({ api_key => '(APIキーの値)', secret => '(Shared secret の値)', perms => 'read', });
という風に、コンストラクタに api_key と secret、perms を指定します。
link_to_login
の中では URI モジュールを使ってリンクのクエリパラメータを組み立てていますが、api_key と perms の他に
api_sig => $self->api_sig($request),
と、API 呼び出し時に必要なシグネチャである api_sig パラメータも指定しています。
api_sig は、この API を第三者に不正に利用されないために、アプリケーションと Flickr 側でやりとりする値。Shared secret の値とリクエストに与えるパラメータを使って、仕様に書かれたようにエンコーディングして渡します。そのシグネチャを作るためのメソッドが api_sig()
です。このメソッドは Flickr::API の sign_args()
をそのままいただきました。
これでアプリケーションを実行するとリンクが表示されるので、そのリンクを踏んで Flickr に飛んでみます。すると、例によって「認証求められているよ、OKかい?」と聞かれるのでそこで OK を押す。
すると、先に管理画面で自分で指定しておいたURLへリダイレクトしてきます。このリダイレクトされてくる URL で認証の続きの処理が実行されるようにアプリケーション側を組み立てておきます。なのでこのURLは認証ハンドラとか Login Callback URL と呼ばれます。
この認証ハンドラへリダイレクトしてくる時に、URL には自動的に「?frob=....」と frob という値が付いてきます。この frob は次の API を呼び出すために必要な値で、Flickr 側が自動で発行してくれるものです。で、認証ハンドラからディスパッチされるロジックでは、
- URL パラメータから frob の値を取得。
- Flickr の API (flickr.auth.getToken) を XML over HTTP で呼び出す。このとき URL パラメータとして frob を指定。
- API の戻りの XML にユーザー名などの情報が入ってるのでそれを parse して値を取得。
<auth> <token>976598454353455</token> <perms>write</perms> <user nsid="12037949754@N01" username="Bees" fullname="Cal H" /> </auth>
という具合でユーザー情報が入ってます。
このAPI周りの扱いですが、
my $frob = $q->param('frob'); my $user = $flickr->user($frob); $user->username;
という感じのインタフェースになるように実装してみました。先のコードに以下のメソッドを追加します。
sub _get_auth_as_xml { my $self = shift; my $frob = shift or croak "You must specify your frob as an argument."; my $uri = URI->new('http://flickr.com/services/rest/'); my $request = { method => 'flickr.auth.getToken', api_key => $self->api_key, frob => $frob, }; $uri->query_form( %$request, api_sig => $self->api_sig($request), ); my $ua = LWP::UserAgent->new; $ua->agent(join '/', __PACKAGE__, __PACKAGE__->VERSION); my $res = $ua->get($uri->as_string); if ($res->is_success) { return $res->content; } else { # FIXME die $res->status; } } sub user { my $self = shift; my $frob = shift or croak "You must specify your frob as an argument"; my $xml = $self->_get_auth_as_xml($frob); # FIXME: parse XML my $user_info = {}; ($user_info->{token}) = $xml =? m!<token>(.*?)</token>!; ($user_info->{perms}) = $xml =? m!<perms>(.*?)</perms>!; ($user_info->{nsid}) = $xml =? m!nsid="(.*?)"!; ($user_info->{username}) = $xml =? m!username="(.*?)"!; ($user_info->{fullname}) = $xml =? m!fullname="(.*?)"!; return unless ($user_info->{username}); return Hatena2::Flickr::API::User->new($user_info); } package Hatena2::Flickr::API::User; use strict; use warnings; use base qw(Class::Accessor::Fast); __PACKAGE__->mk_accessors(qw(token perms nsid username fullname)); 1;
ちと実装が適当な箇所が幾つかありますが、これでうまく動きました。
user
に frob の値を渡して呼び出すと内部では、_get_auth_as_xml
で、flick.auth.getToken を呼び出すための URI 組み立て(flickr.auth.getToken の呼び出しには frob だけでなく、例によって API キーとシグネチャが必要になるので、その組み立てなどを行ってます)と LWP による API 呼び出しをして XML を取得します。この XML を parse (めんどくさいので正規表現で) して、そこから得られた情報で Hatena2::Flickr::API::User をインスタンス化して戻してやる、という具合です。
これでアプリケーションの実装に必要なユーザー名が得られます。あとはアプリケーション側でそのユーザー名を使ってなにがしかの処理をすると。加えて、Cookie を発行するなりして、次回以降は認証フェーズを省くといったことも可能です。
Flickr の認証API では、このあと XML の中にある token を使って、他の API、例えば写真を投稿するとかを実行できます。つまり、サードパーティのアプリケーションに Flickr に登録したメールアドレスやパスワードを要求させずに、認証が必要な処理を token によって実現しているという具合。一方、CLON のコメント欄のように、ユーザー識別のためだけに認証 API を使いたいのであれば、この token を使ってなにがしかの API を呼んだりする必要はなさそうです。
さて、肝心のはてなの認証APIですが、この Flickr の認証APIとほぼ同じことをすれば、とりあえずの要求は満たせるのかなあという雰囲気です。(似たようなことをするのに TypeKey のような仕組みもあります。)
なので、Flickr のような方式でいこうか、とスタッフで話しているのですが、認証API の仕組みにどれだけオリジナリティが必要になるかといったら、別に無駄にオリジナルにする必要はなさそう、むしろインタフェースを同じにしたほうがライブラリなどが使い回せて便利なんじゃなかろうか、という話になりました。
なので、はてなの認証APIも Flickr と同様のインタフェースで実装していこうかなと思っています。一応 Flickr の中の人に問い合わせて、同じインタフェースを採用してもよいかどうか、ちょっと聞いてみたいと思っています。
現在の認証APIプロジェクトの進捗はこんなところです。
あ、作ったクラスのコードの全体は以下です。
package Hatena2::Flickr::API; use strict; use warnings; use base qw(Class::Accessor); use Carp; use URI; use Digest::MD5 qw(md5_hex); use LWP::UserAgent; our $VERSION = 0.01; __PACKAGE__->mk_accessors(qw(api_key secret perms)); sub link_to_login { my $self = shift; my $uri = URI->new('http://flickr.com/services/auth/'); my $request = { api_key => $self->api_key, perms => $self->perms || '', }; $uri->query_form( %$request, api_sig => $self->api_sig($request), ); return $uri->as_string; } sub api_sig { my $self = shift; my $args = shift; my $sig = $self->secret; for my $key (sort {$a cmp $b} keys %{$args}) { my $value = $args->{$key} ? $args->{$key} : ''; $sig .= $key . $value; } return Digest::MD5::md5_hex($sig); } sub _get_auth_as_xml { my $self = shift; my $frob = shift or croak "You must specify your frob as an argument."; my $uri = URI->new('http://flickr.com/services/rest/'); my $request = { method => 'flickr.auth.getToken', api_key => $self->api_key, frob => $frob, }; $uri->query_form( %$request, api_sig => $self->api_sig($request), ); my $ua = LWP::UserAgent->new; $ua->agent(join '/', __PACKAGE__, __PACKAGE__->VERSION); my $res = $ua->get($uri->as_string); if ($res->is_success) { return $res->content; } else { # FIXME die $res->status; } } sub user { my $self = shift; my $frob = shift or croak "You must specify your frob as an argument"; my $xml = $self->_get_auth_as_xml($frob); # FIXME: parse XML my $user_info = {}; ($user_info->{token}) = $xml =~ m!<token>(.*?)</token>!; ($user_info->{perms}) = $xml =~ m!<perms>(.*?)</perms>!; ($user_info->{nsid}) = $xml =~ m!nsid="(.*?)"!; ($user_info->{username}) = $xml =~ m!username="(.*?)"!; ($user_info->{fullname}) = $xml =~ m!fullname="(.*?)"!; return unless ($user_info->{username}); return Hatena2::Flickr::API::User->new($user_info); } package Hatena2::Flickr::API::User; use strict; use warnings; use base qw(Class::Accessor::Fast); __PACKAGE__->mk_accessors(qw(token perms nsid username fullname)); 1;
例によってところどころ記号が "?" となっちゃってるので脳内補完してください。他のMacOSX の人はこの問題どうしてるんだろ...。
Plain Old XML / Plain Old ほげほげ
Someone recently asked me about how to handle an internal product debate around REST vs. SOAP.
In hopes I never have to address this debate again, here's a record of what I told them.
Don Box が REST vs SOAP についての Pragmatics について語っている、という記事。この記事を読む前に OPC Diary: SOAP vs REST? いいから出荷しろ という記事をコメントまで含めて読んでおくと良い感じで消化できる、と思います。
で、あんまり記事とは関係ないお話で。POX - Plain Old XML という単語を恥ずかしながら初めて聞いたもので、そこに反応。
Plain Old XML は
Plain Old XML (POX) is a term used to describe basic XML, sometimes mixed in with other, blendable specifications like XML Namespaces, Dublin Core, XInclude and XLink. People typically use the term as a contrast with complicated, multilayered XML specifications like those for Web Services or RDF.
と Wikipedia にあるように、マルチレイヤな XML 仕様に対比して使われる言葉のようです。REST の話とかでよく出てくるっぽい。
僕は以前に XML 開発者の日で、特に標準化とかもされておらず、マルチレイヤでない、データ構造を表現しただけのXMLみたいなものを指した「野良XML」という言葉を勝手に使ってみたことがあります。POX 言葉のラベルだけをみたときはそういう意味かなと思ったけど、そこまで適当な意味合いじゃなくて、先の Don Box の記事とかを見てると、SOAP とか WSDL とか何か対比される材料があって、あくまでその前提で Plain Old XML という言葉を使ってるみたいですね。
なんで、おそらく Atom フィードみたいにちゃんとどこかで合意の取れた仕様がありつつマルチレイヤでないものも、SOAP なんかと対比されると POX の範疇に入る...という理解。これでよろしい?
プログラミングの世界で "Plain Old 〜" って言うと最近一番よく聞くのは POJO だと思います。Plain Old Java Object。「昔ながらのただの Java オブジェクト」とキーワードページにありますね。Plain Old Java Object は少し前の EJB とかへのアンチテーゼってことですよね、たぶん。
枯れて成熟している技術に対して、新しくやや複雑な技術などがある。そのときに何か後者の方が新しいし複雑ですごそうだからという理由でそちらがもてはやされたときに、「ええ、でもあっちのほうが枯れてて使い易いんだけど...、でもほんとにそれでいいのか自信がないなあ」みたいな考えを後押しするのに Plain Old という修飾語を付けるといいのかもしれない、とたつをさん風に分析してみる。
似たような話題で「何か複雑で新しいものを盲信するのは良くない」っという話題が Strutsの問題点 というプログラマー日記の記事であって、面白かった。(ちとコメント欄で議論があったようなので、結果としての24日の分も併せて読むと良いと思います。)
あと、ちょっとウェブアプリケーション作って試したいなあなんて思う時には CGI で簡単なスクリプトをでっちあげるというのを良くやりますけど、なんか Quick Hack とは言え今時 CGI ってなあ〜...みたいな迷いがある、そんなときは Plain Old Web Application (POWA) とか言っちゃえばいいんじゃないかとかくだらないことを思いついてしまった。Plain Oldメソッド。
ところで Perl ハカーに "Plain Old" でなじみの深いものと言えば POD - Plain Old Document なんですけど、これは何に対する対比なんでしょうね。