「OpenIDの業界団体、4月に日本支部設立へ - ITmedia エンタープライズ」とかで何かとOpenIDが話題になっているので、今更ながらOpenID Consumerのサンプルコードを書いてみた(しかもさらに今更ながら OpenID 1.1 対応)。
Perlには Net::OpenID::Consumer とかライブラリがあるので、本来はそれを使えばいいんだろうけど、OpenIDの動作原理を確認したかったので、完全に手作り。
#!/usr/bin/perl -T use strict; use CGI; use CGI::Carp qw(fatalsToBrowser); use LWP::UserAgent; use HTTP::Request::Common qw(GET POST); use vars qw($UA); sub get_idp { my ($openid_url) = @_; $openid_url = "http://$openid_url" if ($openid_url !~ m|^http://|); my $req = GET $openid_url; my $res = $UA->request($req); return if (! $res->is_success); my $idp = { identity => $openid_url }; my @link = $res->header('Link'); foreach my $link (@link) { $link =~ /^<(.*?)>;.* rel="openid\.server"/ and $idp->{server} = $1; $link =~ /^<(.*?)>;.* rel="openid\.delegate"/ and $idp->{identity} = $1; } return $idp if ($idp->{server}); } sub checkid_immediate { my ($server, $identity, $return_to) = @_; my %param = ( 'openid.mode' => 'checkid_immediate', 'openid.identity' => $identity, 'openid.return_to' => $return_to ); my $query; foreach my $key (keys %param) { $query .= $query ? '&' : '?'; $query .= "$key=$param{$key}"; } return $server.$query; } sub check_authentication { my ($server, $openid) = @_; my $req = POST $server, [ 'openid.mode' => 'check_authentication', 'openid.assoc_handle' => $openid->{assoc_handle}, 'openid.sig' => $openid->{sig}, 'openid.signed' => $openid->{signed}, 'openid.identity' => $openid->{identity}, 'openid.return_to' => $openid->{return_to}, ]; my $res = $UA->request($req); return if (! $res->is_success); my $authentication = {}; my $content = $res->content; foreach my $line (split(/\n/, $content)) { my ($key, $value) = split(/:/, $line, 2); $authentication->{$key} = $value; } return $authentication; } sub openid_form { my ($cgi, $message) = @_; return $cgi->start_html(-title=>'Test OpenID') . $cgi->h1($cgi->a({href=>$cgi->url},'Test OpenID')) . $cgi->p($message) . $cgi->start_form(-method=>'post', -action=>$cgi->url) . $cgi->textfield(-name=>'openid_url') . $cgi->submit(-value=>'LOGIN') . $cgi->end_form . $cgi->end_html; } my $cgi = new CGI; $UA = new LWP::UserAgent; my $message; if ($cgi->request_method eq 'POST') { my ($openid_url, $idp, $url); $openid_url = $cgi->param('openid_url') and $idp = get_idp($openid_url) and $url = checkid_immediate( $idp->{server}, $idp->{identity}, $cgi->url) and print $cgi->redirect($url) and exit; $message = 'Identity Provider Not Found.'; } elsif ($cgi->param('openid.mode') eq 'id_res'){ if (my $url = $cgi->param('openid.user_setup_url')) { print $cgi->redirect($url); exit; } my $openid; foreach my $key ($cgi->param) { my $value = $cgi->param($key); $key =~ s/^openid\.// or next; $openid->{$key} = $value; } my ($idp, $auth); $idp = get_idp($openid->{identity}) and $auth = check_authentication($idp->{server}, $openid) and $message = ($auth->{is_valid} eq 'true') ? 'Login!' : 'Login Failed.' or $message = 'Identity Provider Not Found.'; } print $cgi->header, openid_form($cgi, $message);
上記のコードはdumbモードの実装。dumbモードのシーケンスは以下のような感じ。
End User User-Agent Consumer Identifier Identity Provider | | | | | | openid_url | | | | |----------->| openid_url | | | | |----------->* | | | | | get_idp | | | | |----------->| | | | redirect |<-----------| | | |<-----------| | | | | | | | | | checkid_immediate | | |------------------------------------->| | | | | | login | |<-------------------------------------| | |----------->* | | | | | | | | | redirect | | | | |<-----------| | | | | checkid_setup | | |------------------------------------->| | | | | | setup | |<-------------------------------------| | |----------->* | | | | | get_idp | | | | |------------------------>| | | |<------------------------| | | | check_authentication | | | |------------------------>| | | |<------------------------| | |<-----------| | | |<-----------| | | | | | | | |
Consumerには * のポイントで処理のトリガがかかる。IdP側がsetup済みの場合は、checkid_setupのシーケンスは発生しない。
dumbモードの場合、associate をしない代わりに最後に check_authenticationが必要。これをやらないと偽装したassoc_handleを使ってログインされてしまうということなんだと理解。
ちゃんとセッション管理していれば、2回目のget_idpは不要。サンプルコードは手抜きなので、2回get_idpしてます。しかも1回目と2回目で問い合わせ先が違う場合があるけれど、本質じゃないからまあいいか。