Hatena::ブログ(Diary)

naoyaのはてなダイアリー

February 26, 2006

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 に

  • api_key (APIキー)
  • perms (その認証で必要な権限。read / write など)
  • api_sig (シグネチャ。後述)

の三つのパラメータをクエリパラメータで指定したリンクを用意します。このために 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 して値を取得。

ということをします。API の戻りとしての XML には

<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 を呼んだりする必要はなさそうです。

以上、Flickr の認証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 の人はこの問題どうしてるんだろ...。

yune_kotomiyune_kotomi 2006/02/26 13:20 記号が化けそうなときはFirefoxに切り替えて書き込んでます。SafariならDebug→Open Page With、シイラなら表示→他のブラウザで表示、から表示中のページを他のブラウザで開けるので、そうやって切り替えます。対症療法なのが気に入りませんが。

naoyanaoya 2006/02/26 13:28 あ、Firefox なら化けないんですね。知らなかったワー。

miyagawamiyagawa 2006/02/26 15:56 FlickrのAPIはデスクトップアプリやモバイルにも対応させるためにかなり冗長になっている気がします。実際に使ってみたけどあまり使いやすくはなかったです。

あとfrobが漏れると普通に認証できてしまうのでWebの場合はRefererなどに注意が必要なところが弱い気がしました。

デベロッパーや、ユーザとアプリケーションのpermissionのon/offなどをFlickr側で管理しているので、最初に立ち上げるにしてはかなりオーバースペックなんじゃないかと、傍目から見ると思いますけど、どうでしょうね。

naoyanaoya 2006/02/26 16:24 API のラッピングを自分で書いた分には、API を処理する部分に関してはそれほど実装コストが高くないなという印象でしたが、それを色んなアプリケーションに組み込むにあたってどういう感想かはまだちょっと掴めてない感じです。CLON のようなことをするのであれば特に使いにくい、という感じはなさそうですが。

frob 漏れに関しては、frob を一回限定とか少し工夫は要りそうですね。

管理システムがオーバースペックというのは、アプリケーションを作るときにデベロッパがめんどくさいと感じる、ということですか?

一方のサーバーサイドである僕らからみると管理システムの工数がちょっと高めかなという印象はありますが、そこはクライアントに楽させるために少し高めでも実装すればいい、という風に思います。

miyagawamiyagawa 2006/02/26 18:50 そうそう、デベロッパーがAPI Key登録をして、複数のサイトを登録できて、かつユーザがそれぞれのサイトに対してどのビット(perm)で許可しているかを管理しているので、そこまでマネするのはちょっとしんどそうだなあ、と思いました。はてな側で問題ないなら、余計なお世話ですけどw

frob はすでにFlickrではonetimeでexpireするようになっていたみたいですね。

naoyanaoya 2006/02/27 00:57 frob の件も含めて、サーバー側で実装する必要のある機能を色々考えてみると確かにめんどくさそうですね。

要件洗い出してどの程度なものかもうすこし詳細に考えてみます。

fumiakiyfumiakiy 2006/02/27 10:56 僕はFlickr改に1票。認証後の_returnによる任意のURLへのリダイレクト、またはベースURLはあらかじめ指定しておく(Flickr方式)にしても、クエリ文字列はあとから追加できるようにしてほしいです。Flickrにはこれがないので、MTコメント認証のときパーマリンクごとに異なるURLへリダイレクトできなくて、苦肉の策でデスクトップアプリです、ってことにして、認証後にリダイレクトしないようにして、あとでユーザーが能動的にFrobからTokenを取得するようにしてごまかしてます。

Windowsアプリも作る人からすると、frobを使ってあとから承認状況を取得できるFlickr方式はうれしいですね。Webアプリだけをターゲットにするなら不要ですけど。