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

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

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

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

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

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

ではどう対応するかと

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

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


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

```omniauth-facebook``` の READMEには [Canvas Apps](https://github.com/mkdynamic/omniauth-facebook#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
```