TwitterとかGitHubユーザに対して自前のWebアプリケーションでOAuth認証するためのPlack::Middlewareを書いてる.
同様の動作をするモジュールとしてすでに
Plack::Middleware::OAuth
があったけど,Session周りでよくわからないエラーがでてよくわからなかったから,自分で書き始めた.
(よくわからないエラーというのは,$env->{psgix.session}と$env->{psgix.session.options}がundefになってて,Plack::Session->new($env)したときに,これらがHASHリファレンスじゃないと怒られる感じだった.
結局,Middlewareの読み込み順が間違ってて,Plack::Middleware::Sessionを先に書いてないだけだった.
Plack::Middleware::OAuthはPlack::Middleware::Sessionまたは他のSessionストレージの使用が前提になっている気がする.
builder {
enable "Plack::Middleware::Session",
...
enable 'Plack::Middleware::OAuth',
...
$app
};
)
Plack::Middleware::OAuth::Liteは,各プロバイダのエンドポイントなどの設定を内部に持っていなくて,enable時に設定を書くようになっている.
あと,OAuth2.0には対応してない.
OAuth2.0はシグネチャのためのハッシュ値計算みたいなのをやらなくて良いから外部モジュール使わずに手で書けばいい気がする.
OAuth認証まわりはOAuth::Lite::Consumerを使ってる.
書いてみたはいいけど,Plack::Middleware::OAuthのほうがあきらかによいインタフェースだということがわかってよかった.
ただ,Plack::Middleware::OAuthのコールバックルーチンに渡される第一引数$selfにはredirectとかrenderとかが生えていて,これらは自前で実装してるようなので,この辺うまくPlack::Requestを使えなかったんだろうか.
Plack::Middleware::OAuthは内部でいろいろよしなにやってくれる感じで自由度が低めに感じたので,外からいろいろ設定を渡せるようにしたけど,なんだか微妙な感じになった感がある.
Plack::Middleware::OAtuhを使いましょう.
psgiファイルの設定
enable "Plack::Middleware::Session";
enable 'Plack::Middleware::OAuth::Lite',
on_success => sub {
my ($res, $provider_name, $token, $user_info, $location) = @_;
$res->redirect("/auth/signup/$provider_name");
return $res;
},
on_error => sub {
my ($res, $error) = @_;
$res->redirect("/oauth/error?reason=".$error);
return $res;
},
providers => {
'twitter' => {
consumer_key => 'XXXXXX',
consumer_secret => XXXXXX',
site => 'http://api.twitter.com/',
request_token_path => 'https://api.twitter.com/oauth/request_token',
access_token_path => 'https://api.twitter.com/oauth/access_token',
authorize_path => 'https://api.twitter.com/oauth/authorize',
login_path => '/auth/twitter',
scope => '',
user_info_uri => qq{https://api.twitter.com/1.1/account/verify_credentials.json},
user_info_uri_method => 'GET',
},
hatena => {
consumer_key => 'XXXXXX',
consumer_secret => 'XXXXXX',
site => qq{https://www.hatena.com},
request_token_path => qq{https://www.hatena.com/oauth/initiate},
access_token_path => qq{https://www.hatena.com/oauth/token},
authorize_path => qq{https://www.hatena.ne.jp/oauth/authorize},
login_path => '/auth/hatena',
scope => 'read_public',
user_info_uri => qq{http://n.hatena.com/applications/my.json},
user_info_uri_method => 'POST',
},
};
package Plack::Middleware::OAuth::Lite;
use utf8;
use strict;
use warnings;
our $VERSION = '0.01';
use parent 'Plack::Middleware';
use Plack::Util;
use Plack::Util::Accessor qw(providers on_success on_error flash_success_message);
use Plack::Request;
use Plack::Session;
use Carp ();
use JSON::XS qw(decode_json);
use OAuth::Lite::Consumer;
use Try::Tiny;
our $CONFIG_KEYS = [qw(
consumer_key
consumer_secret
site
request_token_path
access_token_path
authorize_path
login_path
scope
user_info_uri
)];
sub prepare_app {
my ($self) = shift;
my $p = $self->providers || Carp::croak "require providers";
$self->{client_info} ||= {};
$self->{login_path_info} ||= {};
$self->{scope_info} ||= {};
$self->{user_info} ||= {};
for my $provider_name (keys %$p) {
my $config = $p->{$provider_name};
check_config($config);
my $lc_name = lc($provider_name);
$self->{client_info}{$lc_name} = OAuth::Lite::Consumer->new(
consumer_key => $config->{consumer_key},
consumer_secret => $config->{consumer_secret},
site => $config->{site},
request_token_path => $config->{request_token_path},
access_token_path => $config->{access_token_path},
authorize_path => $config->{authorize_path},
);
$self->{login_path_info}{$lc_name} = $config->{login_path};
$self->{scope_info}{$lc_name} = $config->{scope};
$self->{user_info}{$lc_name} = {
uri => $config->{user_info_uri},
method => $config->{user_info_uri_method} || 'GET',
};
}
}
sub check_config {
my ($config) = @_;
for my $expected (@$CONFIG_KEYS) {
unless (grep {$expected eq $_} keys %$config) {
Carp::croak "require $expected";
}
}
}
sub call {
my ($self, $env) = @_;
my $session = Plack::Session->new($env);
$self->{handlers} //= do {
my $handlers = {};
while (my ($provider_name, $login_path) = each %{$self->{login_path_info}}) {
$login_path =~ s!(.+)/$!$1!;
my $callback_path = "$login_path/callback";
my $consumer = $self->{client_info}{$provider_name};
$handlers->{$login_path} = sub {
my ($env) = @_;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
my $request_token = $consumer->get_request_token(
callback_url => _callback_uri($req->base, $callback_path),
scope => $self->{scope_info}->{$provider_name} || undef,
) or die $consumer->errstr;
$session->set($provider_name.'oauth_request_token' => {%$request_token});
$session->set($provider_name.'oauth_location' => $req->param('location'));
$res->redirect($consumer->url_to_authorize(token => $request_token));
return $res->finalize;
};
$handlers->{$callback_path} = sub {
my ($env) = @_;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
if ($req->param('denied')) {
return $res->redirect('/');
}
my $verifier = $req->param('oauth_verifier')
|| die "No oauth verifier";
my $access_token = $consumer->get_access_token(
token => (bless $session->get($provider_name.'oauth_request_token'), 'OAuth::Lite::Token'),
verifier => $verifier,
) or die $consumer->errstr;
$session->remove($provider_name.'oauth_request_token');
$session->set($provider_name.'oauth_access_token', {%$access_token});
{
my $u = $self->{user_info}{$provider_name};
my $u_res = $consumer->request(
method => $u->{method}, url => $u->{uri}, token => $access_token,
);
$u_res->is_success or die "failed getting user info";
my $user_info = eval { decode_json($u_res->decoded_content || $res->content) };
$session->set($provider_name.'oauth_user_info', $user_info);
my $location = $session->get($provider_name.'oauth_location') || "/";
$res = $self->on_success->($res, $provider_name, $access_token, $user_info, {
location => $location,
});
}
return $res->finalize;
};
}
$handlers;
};
return $self->_run($env, $self->{handlers});
}
sub _run {
my ($self, $env, $handlers) = @_;
my $app = $handlers->{$env->{PATH_INFO}};
return $self->app->($env) unless $app;
my $res;
try {
$res = $app->($env);
} catch {
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
$_ =~ /(.*)\sat/;
$res = $self->on_error->($res, $1);
return $res->finalize;
}
$res;
}
sub _callback_uri {
my ($base_uri, $callback_path) = @_;
$callback_path =~ s!^/?(.+)!$1!;
$base_uri . $callback_path;
}
1;