Facebook内で使うタイプのアプリで認証にAmon2::Authを使う方法

前回は外部のFacebookアプリの認証をやって、それを内部でやろうとしてうまくいかなかったということを書きました。
Amon2を使って!DotCloudで!Facebook アプリを作りたい!(まだ試し中) - hsksnote
今回はそれがうまくいったので、ソースと一緒に解説します。


前提として、Facebook内で動かすアプリは、Facebookのキャンバスページ内のiframeに表示されます。なので、ローカルでテストできません。これが非常に面倒くさい。
今回はDotCloudにテストアプリを作って動かしました。


Facebook側の設定は、こちらの記事をご参照。キャンバスURLを設定してやる必要があります。
Apps on Facebook.com : Facebook開発者向けドキュメントの日本語訳とTips


まずは認証につかうAmon2プラグインへの依存を書きます。
Makefile.PL

         'Plack::Session'                  => '0.14',
         'Test::WWW::Mechanize::PSGI'      => '0',
         'Time::Piece'                     => '1.20',
+               'Amon2::Plugin::Web::Auth'        => '0',
+               'Amon2::Auth::Site::Facebook'     => '0',
     },


コンフィグファイルに認証用の設定を書きます。callback_uriにキャンバスページのURLを設定するのがポイント。Facebookのアプリ設定画面にURLが出てます。
dotcloud上で動かすので、development.plだけじゃなくdeployment.plにも同じ事を書く。
config/deployment.pl & config/development.pl

             sqlite_unicode => 1,
         }
     ],
+       Auth => {
+               Facebook => {
+                       client_id => 'xxxxxxxxxxxxxxx',
+                       client_secret => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
+                       callback_uri => 'http://apps.facebook.com/hsksyusk_sample_app/',
+                       scope => 'read_stream',
+               }
+       }
 };


プラグインを読み込みます。redirect先にも、キャンバスページのURLを指定。
lib/fbsample/Web.pm

+__PACKAGE__->load_plugin(
+       'Web::Auth',
+       {
+               module => 'Facebook',
+               on_finished => sub {
+                       my($c, $token, $user) = @_;
+                       my $name = $user->{name} || die;
+                       $c->session->set('name'  => $name );
+                       $c->session->set('site'  => 'facebook');
+                       $c->session->set('token' => $token);
+                       return $c->redirect('http://apps.facebook.com/hsksyusk_sample_app/');
+               },
+               on_error => sub {
+                       my ($c, $error ) = @_;
+                       warn ("auth_error!![$error]");
+                       return $c->redirect('/');
+               },
+       }
+);

ここでURL末尾の/を書いてなくてずっとハマってた。


今回作ってるWebアプリは、FacebookのキャンバスページからはPOSTで呼ばれます。
おなじくWeb.pmで、Web::CSRFDefenderプラグインの読み込みをコメントアウトします。
lib/fbsample/Web.pm

 # load plugins
 __PACKAGE__->load_plugins(
     'Web::FillInFormLite',
-    'Web::CSRFDefender',
+    # 'Web::CSRFDefender',
 );


HTTPレスポンスヘッダーのX-Frame-Optionsの設定を無効にします。
このオプションは、iframeで読み込まれることを許可しない設定で、クリックジャッキング攻撃を防ぐ目的で使われるのだとか。
lib/fbsample/Web.pm

         $res->header( 'X-Content-Type-Options' => 'nosniff' );

         # http://blog.mozilla.com/security/2010/09/08/x-frame-options/
-        $res->header( 'X-Frame-Options' => 'DENY' );
+        # $res->header( 'X-Frame-Options' => 'DENY' );

このへん、セキュリティ的に無防備になってしまうところは、呼び出し元をfacebookからに限るとか、対策を考えないといけないですね。


ここまでで設定が終わったので、処理を書いていきます。
と言っても、こちらの記事のコードそのままでいけます。
Amon2でFacebookAPIを使う その2 - Perl勉強メモ アルパカDiary出張版
lib/fbsample/Web/Dispatcher.pm

+use JSON qw(decode_json);

 any '/' => sub {
     my ($c) = @_;
-    $c->render('index.tt');
+       my $sign = $c->req->param('signed_request');
+       my $data;
+       my $token = $c->session->get('token');
+       my $sign = $c->req->param('signed_request');
+       if( $token ) { #loggedin
+               my $ua = LWP::UserAgent->new();
+               my $res = $ua->get("https://graph.facebook.com/me/home?access_token=${token}");
+               $res->is_success or die $res->status_line;
+               $data = decode_json($res->decoded_content);
+
+               $c->render(
+                       'index.tt',
+                       {
+                               name => $c->session->get('name'),
+                               data => $data->{data},
+                       }
+               );
+       }
+       else {
+               $c->redirect('/auth/facebook/authenticate');
+       }
+};

 post '/account/logout' => sub {
     my ($c) = @_;
     $c->session->expire();

DotCloud特有なのかわかりませんが、リクエストがPOSTで来ると、responseが全部来る前にセッションが完了しちゃうという問題がありまして。それに対応するために、$c->req->param('signed_request')を変数に入れてます。こうやって明示的にパラメータを読んでやることで、この問題を回避することができるみたい。


HTMLテンプレートはこんな感じ。
tmpl/index.tt

[% WRAPPER 'include/layout.tt' %]
<section>
[% IF data %]
        Hi! [% name %]
        <section>
                <ul>
                        [% FOR v IN data %]
                        <li>[% v.created_time %] [% v.from.name %] [% v.message %]</li>
                        [% END %]
                </ul>
        </section>
[% END %]
</section>
[% END %]


cssで幅をiframeに合わせます。
static/css/main.css

+.container {
+       width: 740px;
+}


これで全部です。git addしてgit commitしてdotcloud pushしてFacebookのキャンバスページにアクセスしたら、リダイレクトして認証したあとにキャンバスページに戻ってきて、自分のニュースフィードの中身が表示されるという寸法です。