Facebook
3rd-party-cookie
iframe

Facebook ページタブアプリやらCanvasアプリでOAuthするときに3rd party cookie 問題で死にかけた話

More than 3 years have passed since last update.

ほら、 Facebook の canvasアプリ やら page tabアプリ やら作る時って、 iframe にサイトを表示させるじゃないですか。

そうすると、 3rd party cookie について
思いを馳せないとならんわけですよ。

普通に、 OAuth すると、facebookiframe x-frame-options のセキュリティー・ポリシーにより、画面が遷移しない。

ここで、なぜ自分の作ったFacebookアプリが facebookiframe x-frame-options のセキュリティー・ポリシーにかかるかというと、Facebook ページタブ内のページは facebook のプロキシ介しているんですね。

なので、そういう事が起こる。

ではどう対応するかと

  • omniauthの iframe オプションで回避
    • これだと topwindowOAuth できるんだけど、返ってきたあとFacebook ページタブではなく普通に外部サイトに返ってしまう。
    • cookieが別になってしまう
  • FB JavaScript SDK で先に OAuth してしまう。
    • 今回はこれ
    • 目からウロコだった

テスト書くのが結構たいへんだったけど
FB JavaScript SDKstub 使ったり、して何とか対応出来た。

FB JavaScript SDK で先に OAuth してしまう

omniauth-facebook の READMEには Canvas Apps の項がありまして、コードになおすると以下の様な感じでいけると書いてあるですよね。

FB.init(appId: FB_APP_ID, status: true, cookie: true, xfbml: true);
FB.login(function(response) {
   if (response.authResponse) {
     location.href = '/auth/facebook'; // omniauth-facebook で生やした認証URLへ移動するだけ
   } else {
     console.log('User cancelled login or did not fully authorize.');
   }
});

まぁ確かにイケますよ。3rd party cookie を許可していれば。

問題は許可していない場合で、色々検討した結果、
その時のフローは以下のようにすると良さそうだということが分かりました。

  • facebook javascript sdk の FB.login or FB.loginStatus のコールバックで signed_request を取得。
  • サーバーに signed_request を送信。
  • サーバー側でパースして code を取得。
  • code から access_token を取得。
  • access_token を使って FB graph の /me で自身の情報を取得。
  • omniauth の OAuth がコールバックで受け取るかたちに整形して、認証。
  • cookie 使えないので毎回 signed_request を送信して上記のフローで認証。

具体的にどうやるんだ

Railsと連携する想定のコードはこんな感じ。
色々調べて分かったけど、コードまで明示しているところがさっぱりなかったのでメモとして記録しておきます。(そんなときの qiita だよね!!!)

client-side.js
FB.init(appId: FB_APP_ID, status: true, cookie: true, xfbml: true);
FB.login(function(response) {
   if (response.authResponse) {
     var uid = response.authResponse.userID;
     var signed_request = response.authResponse.signedRequest;
     // /users/:id にしたいがためにuid取得してる
     var url = 'http://example.com/users/' + uid;
     // ここで access_token 取れるけどそれを生でサーバーに送っちゃダメよ
     $.ajax({
       url: url,
       method: 'post',
       data: {
         signed_request: signed_request,
         _method: 'put' # rails  PUT
       },
       success: function() {
         // 認証後の何か
       }
     });
   } else {

   }
});
server-side.rb
class UsersController
  # これ微妙と思うなら、form用意しておいて、そこから $("input[name='authenticity_token']").val() して渡すと良い
  skip_before_filter :verify_authenticity_token

  def update
    # FBアプリの credential は figaro gem で管理している想定
    # FB api は Koala gem で操作する想定
    oauth = Koala::Facebook::OAuth.new(Figaro.env.fb_app_id, Figaro.env.fb_app_secret)

    # signed_request をパース
    res = oauth.parse_signed_request(params[:signed_request])

    # access_token を取得
    access_token = oauth.get_access_token(res['code'])

    # sigend_request を送ってきた奴の情報を取得
    graph = Koala::Facebook::API.new(access_token)
    user_info = graph.get_object('me?locale=ja_JP')

    # omniauth で /auth/facebook/callback が受け取るenv['omniauth.auth'] のhash形式
    # https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema に変換
    auth = 
    {
      'uid' => uid,
      'info' => {
        'name' => user_info['name'],
        'nickname' => user_info['name'],
        'image' => "https://graph.facebook.com/#{uid}/picture",
        'email' => user_info['email'],
      },
      'credentials' => {
        'token' => access_token,
      },
    }

    # authを使ってユーザー作成 or 認証
    User.create_or_udpate_by_auth(auth)
    head :ok
  end
end