POEed OpenID consumer Server

Web API で遊び倒す

などという何だかよくわからない演目を某Conference用に出してしまった私.いつものように,妙な演目を考えた自分を呪いながら四苦八苦しております.web apiと言っても,もしかしたらConference会場ではINTERNETに抜けられない状況があるかもしれないし,はたまたMashup Awardに出したら審査員が一瞥の下にゴミ箱に突っ込んでしまうようなwebアプリを作るってのもしゃくに触ります.そんなこんなで悩んだあげく,OpenIDを使って何か作ってみようと思い立ちました.

Net::OpenID::Consumer

とりあえず,と思ってCPANを漁って見ると,あるじゃあありませんか.その名も"Net::OpenID::Consumer".っても,その他に"Net::OpenID::JanRain::Consumer"とか"Net::OpenID::Consumer::Yadis"なんてのも転がっています.でもまぁ,そこは勘で「これだ」と思ったモジュールを使ってみるのです.

間違ってたら,そんときに考えればいいんだし.

さてこの,"Net::OepnID::Consumer"作ったのはBrad Fitzpatrick さんです.なんてことでしょう.ドキュメントが異常に少なくって涙がちょちょぎれるモジュールを大量生産している"six apart"の方ではないですか.(中身がわかるようになればそれで十分だと思うんだけど,初見ではチンプンカンプン)pod見ながらうなるより,ぐぐってサンプル見つける方が確実,と思って見つけたのがOpenID guestbookでした.ありがたいことにこのサービス,ソースコードも公開してくれていました.これなら簡単.後は余分な所をはぎ取って,自分なりのエンジンを作ってしまえばいいのです.cgiスクリプトなんてやめてPOEで作り直すのぢゃ

POE::Compoennt::Server::HTTP

POEでcgiみたいなものを動かして,あまつさえcookieのハンドリングなんてだれかやってるかしらん.と思った時にふと思い出したのがhttp://naoya.g.hatena.ne.jp/naoya/20061113/1163418064でした.HTTP::Request::AsCGIモジュールと組み合わせれば,CGIモジュールも使えるしcookieの取り扱いも大丈夫.こんなのちょちょいのちょいです.

・・・,うそです.茨の道でした.

え〜と,気を取り直して,これが作り上げたスクリプトです.

#!/usr/local/bin/perl
use strict;
use Net::OpenID::Consumer;
use POE qw/Component::Server::HTTP/;
use CGI qw(:standard);
use CGI::Cookie;
use Digest::SHA1 qw(sha1 sha1_hex);
use HTTP::Request::AsCGI;
use Smart::Comments;

my $this_script   = "http://bar.foo.co.jp:10080";
my $required_root = "http://bar.foo.co.jp:10080";
my $trust_root    = "http://bar.foo.co.jp:10080";

my $aliases = POE::Component::Server::HTTP->new(
    Port           => 10080,
    ContentHandler => { '/' => \&handler },
);
my %table;

POE::Kernel->run;

sub handler {
    my ( $req, $res ) = @_;
    my $c = HTTP::Request::AsCGI->new($req)->setup;
    my $q = CGI->new;

    my $con = Net::OpenID::Consumer->new(
        ua              => LWP::UserAgent->new,
        cache           => undef,
        args            => $q,
        consumer_secret => \&mysecret,
        required_root   => $required_root,
    );

    if ( $q->cookie('openid') ) {
### processing cookie, openid...
        if ( $q->param('logout') ) {
### processing logout...
            $table{ $q->cookie('openid') } = undef;
            print $q->redirect(
                -url    => $this_script,
                -cookie => new CGI::Cookie(
                    -name    => 'openid',
                    -value   => "",
                    -expires => '-1d',
                ),
            );
            $res->code(302);
        }
        else {
### processing default page...
            print $q->header;
            print $q->start_html( -title => "OpenID Test" );
            print $q->h1("OpenID Verified");
            print $q->start_form;
            print $q->submit('logout');
            print $q->end_form;
            print $q->end_html;
            $res->code(200);
        }
    }
    elsif ( $q->param() ) {
        if ( $q->param('ret') ) {
### processing ret...
            if ( my $setup_url = $con->user_setup_url ) {
### RET: $setup_url
                print $q->redirect( -url => $setup_url );
                $res->code(302);
            }
            elsif ( $con->user_cancel ) {
### processing cancel...
                print $q->redirect($this_script);
                $res->code(302);
            }
            elsif ( my $vident = $con->verified_identity ) {
                my $verified_url = $vident->url;
### VERIFIED: $verified_url
                $table{$verified_url} = "";
                print $q->redirect(
                    -url    => $this_script,
                    -cookie => new CGI::Cookie(
                        -name  => 'openid',
                        -value => $verified_url
                    ),
                );
                $res->code(302);
            }
        }
        elsif ( $q->param('openid_url') ) {
### processing user input url...
            my $claimed   = $con->claimed_identity( $q->param('openid_url') );
### CLAIMED: $claimed
            my $check_url = $claimed->check_url(
                return_to  => $this_script . "?ret=true",
                trust_root => $trust_root,
            );
### CHECK: $check_url
            print $q->redirect( -url => $check_url );
            $res->code(302);
        }
    }
    else {
### print login page...
        print $q->header;
        print $q->start_html( -title => "OpenID test" );
        print $q->h1("OpenID TEST");
        print $q->start_form( -name => "openid_url" );
        print "enter OpenID: ";
        print $q->textfield(
            -name => 'openid_url',
            -size => 35,
            -style =>
'background: url(http://stat.livejournal.com/img/openid-inputicon.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px;'
        );
        print $q->submit( -name => "verify" );
        print $q->end_form;
        print $q->end_html;
        $res->code(200);
    }

    my $c_res = $c->restore->response;
    $res->content( $c_res->content );
    $res->{_headers} = $c_res->{_headers};
    $res->{_msg}     = $c_res->{_msg};
    $res->{_rc}      = $c_res->{_rc};
    return RC_OK;
}

sub mysecret {
    my $t = shift;
    $t = time() unless ($t);
    $t -= $t % 3600;
    return sha1_hex( " foo " . ( $t - $t % 3600 ) );
}

動き

  1. ユーザがアクセスすると,login画面を出します.そこにOpenID urlを書いてやります
  2. ユーザが指定したページをGetして,そこに書いてあるOepnID Server名を探します(claimed_identity)
  3. 戻り先は俺だ,と印をつけて(check_url)OpenID Serverにリダイレクトします
  4. OpenID Serverから,認証するんだったらここに飛べと行ってきたurl(user_setup_url)にリダイレクトします
  5. 認証結果がokかどうかをチェック(verified_identity)して自分自身にリダイレクトします
  6. これで終わり


ってな感じです.このスクリプトのテストにはvoxサービスのOpenID機能を使ってみました.POEの動作を追いかけるためにSmart::Commentモジュールも使っています.動きは,元のguestbookスクリプトをそっくりそのまま踏襲しています.もしかしたら,もっと小さく作れるかもしれないですが,それはまた後日.あ,POEの中でLWP::UserAgentモジュールなんか使いやがって!,ブロックしちゃうじゃないか,っていう意見が出てくるのは承知の上で作っています.まだNet::OpenID::Consumerの中を覗いてないのでそこまで追いきれていません.