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 APIPerl で利用するには 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 %]flickrAPI を抽象化した以下のクラスのインスタンスです。

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::APIsign_args() をそのままいただきました。

これでアプリケーションを実行するとリンクが表示されるので、そのリンクを踏んで Flickr に飛んでみます。すると、例によって「認証求められているよ、OKかい?」と聞かれるのでそこで OK を押す。

すると、先に管理画面で自分で指定しておいたURLへリダイレクトしてきます。このリダイレクトされてくる URL で認証の続きの処理が実行されるようにアプリケーション側を組み立てておきます。なのでこのURLは認証ハンドラとか Login Callback URL と呼ばれます。

この認証ハンドラへリダイレクトしてくる時に、URL には自動的に「?frob=....」と frob という値が付いてきます。この frob は次の API を呼び出すために必要な値で、Flickr 側が自動で発行してくれるものです。で、認証ハンドラからディスパッチされるロジックでは、

  • URL パラメータから frob の値を取得。
  • FlickrAPI (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 の仕組みにどれだけオリジナリティが必要になるかといったら、別に無駄にオリジナルにする必要はなさそう、むしろインタフェースを同じにしたほうがライブラリなどが使い回せて便利なんじゃなかろうか、という話になりました。

なので、はてなの認証APIFlickr と同様のインタフェースで実装していこうかなと思っています。一応 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 の人はこの問題どうしてるんだろ...。